Compare commits
222 Commits
apple-auto
...
preferred-
| Author | SHA1 | Date | |
|---|---|---|---|
|
8e852ed742
|
|||
|
7b4fc79030
|
|||
|
|
7a4af31859 | ||
|
|
e106be1412 | ||
|
|
282bf80daa | ||
|
|
bcb861a61b | ||
|
|
bb0ad18913 | ||
|
|
81830c7540 | ||
|
|
68128b5ff1 | ||
|
|
aebeb26bc6 | ||
|
|
79cf3db279 | ||
|
|
dcae0d2cc7 | ||
|
|
2b12dc5920 | ||
|
|
51930e7a12 | ||
|
|
b04e09d2e0 | ||
| b6c4213515 | |||
|
|
8230c6eded | ||
|
|
e79590f795 | ||
|
|
79bced1246 | ||
|
|
896f4b55e3 | ||
|
|
52e65f9429 | ||
|
|
a22cc532e2 | ||
|
|
823227920c | ||
|
|
3e2bbce25e | ||
|
|
e05b2d9ecf | ||
|
|
d7b31a1cd8 | ||
|
|
70f01c0880 | ||
|
|
2cf5f21f78 | ||
|
|
96e8f8b6b2 | ||
|
|
370cfd1b08 | ||
|
|
046af15734 | ||
|
|
9e4ab2d54c | ||
|
|
7cf12e2e0d | ||
|
|
a63a81b387 | ||
|
|
d994cd13dc | ||
|
|
95e985cfce | ||
|
|
3a69de9274 | ||
|
|
64f5acf98c | ||
|
|
5167ab264d | ||
|
|
e02895b29f | ||
|
|
0009d11025 | ||
|
|
afc317bb52 | ||
|
|
629212ea23 | ||
|
|
ec1252200f | ||
|
|
54ea1ab803 | ||
|
|
4cf8097de4 | ||
|
|
2c7384b0a9 | ||
|
|
19e312a8fb | ||
|
|
3986308638 | ||
| fa7740948b | |||
| 892a1420f3 | |||
| ee4cbf7363 | |||
| a1b1ce949b | |||
| 902e8c3950 | |||
| b776788b38 | |||
|
|
78066773f4 | ||
|
|
0bac284eee | ||
|
|
07c95d1003 | ||
|
|
1072c5a384 | ||
|
|
5ed6e85ad8 | ||
|
|
f948dd81ca | ||
|
|
391818f230 | ||
|
|
dc74ad37a1 | ||
|
|
e1c94b7ff9 | ||
|
|
ec933452d3 | ||
|
|
977b268023 | ||
|
|
0c778af833 | ||
|
|
e75e7950b5 | ||
|
|
8d68297cce | ||
|
|
e0fd24aff5 | ||
|
|
b5c3ff45e4 | ||
|
|
786dbb21c4 | ||
|
|
5a17c330da | ||
|
|
975be63ce1 | ||
|
|
d9796bd63c | ||
|
|
25a835624a | ||
|
|
1d06683bb3 | ||
|
|
8e15a86c0a | ||
|
|
51a3008e5a | ||
|
|
f71c1b9848 | ||
|
|
151e23d524 | ||
|
|
7619891c86 | ||
|
|
cc98525f59 | ||
|
|
8cf9549981 | ||
|
|
9ebf27cd37 | ||
|
|
16a1a9f37f | ||
|
|
08d28b0f00 | ||
| d0ae3ca08a | |||
| 6e2f770876 | |||
|
|
4079bea912 | ||
| 4d01340b90 | |||
|
|
61b89c2f54 | ||
|
|
cbdff4a5f8 | ||
|
|
866afe970b | ||
|
|
87efc91527 | ||
| 4121526588 | |||
| 4adcb738a2 | |||
| 1f17f19a6e | |||
|
|
fb54115286 | ||
|
|
50dd35d089 | ||
|
|
1205b2a0e2 | ||
|
|
0a93e909ed | ||
|
|
6f3f928ac3 | ||
|
|
637ceabede | ||
|
|
c25f54f7e7 | ||
|
|
d4ae0b1346 | ||
|
|
7773618547 | ||
|
|
a4199fa299 | ||
|
|
5b9ccc4ee5 | ||
|
|
f538e03093 | ||
|
|
e625297a2e | ||
|
|
e603678872 | ||
|
|
9d77f1b2f7 | ||
| c4f7d25793 | |||
|
|
100f195a03 | ||
|
|
e599ef1ac9 | ||
|
|
033c69b92e | ||
|
|
184e566b1b | ||
|
|
8c321e479b | ||
|
|
960c84d02e | ||
|
|
02f1c2d342 | ||
|
|
c8ca3c93f6 | ||
|
|
5c6e5ca2de | ||
|
|
e3105a90c5 | ||
|
|
38dc7b046a | ||
|
|
da76ad9b66 | ||
|
|
177c55cf3d | ||
|
|
eeb6547d3e | ||
|
|
50ef6600a8 | ||
|
|
be43819de2 | ||
|
|
58017952bc | ||
|
|
409be7fc58 | ||
|
|
1bc660c9cd | ||
|
|
a56a59f81d | ||
|
|
1d5af6ca5c | ||
|
|
f81b2b677f | ||
|
|
290152c859 | ||
|
|
c4ee52fdac | ||
|
|
6f04455350 | ||
|
|
3b62945e5b | ||
|
|
b1b032d905 | ||
|
|
7c805f7f23 | ||
|
|
a2b0620175 | ||
|
|
8c6bee3d90 | ||
|
|
bba651b37c | ||
|
|
f657af275a | ||
|
|
03ded7d39f | ||
|
|
53edc7eb0b | ||
|
|
8b969021d5 | ||
|
|
1e52982a5d | ||
|
|
0038d42f71 | ||
|
|
d94b387fb9 | ||
|
|
8a2dbc95ca | ||
|
|
5865b000c0 | ||
|
|
6efb512a64 | ||
|
|
b7b8c7f175 | ||
|
|
a76e2aa677 | ||
|
|
d9f2317728 | ||
|
|
f1339e835b | ||
|
|
64f2362be3 | ||
|
5b184a40fd
|
|||
|
|
17e6191a92
|
||
|
|
1ff065d4c7
|
||
|
|
94e2c76284 | ||
|
|
1925af6897 | ||
|
|
4effaa4324 | ||
|
|
e2a4443a9c | ||
|
|
7c0e1c5ded | ||
|
|
f6e34ad999 | ||
|
|
ca5da7b5cd | ||
|
|
c08e4a2fdd | ||
|
|
37a50f6087 | ||
|
|
2040e79165 | ||
|
|
a6449020b6 | ||
|
|
75a46f4ab4 | ||
|
|
c948c7e230 | ||
|
|
c83b0fba21 | ||
|
|
b7053e8680 | ||
|
|
17183632c8 | ||
|
|
686d6d6e92 | ||
|
|
847ae7b396 | ||
|
|
ba9780fb17 | ||
|
|
42f5af0ffd | ||
|
|
55f1330fc1 | ||
|
|
4b326340a3 | ||
|
|
83f7766833 | ||
|
|
1e3b20f5b3 | ||
|
|
e508f28f7d | ||
|
|
2c139863b8 | ||
|
|
c699409129 | ||
|
|
74e6d8781a | ||
|
|
876f9c742f | ||
|
|
1e7b57eaf3 | ||
| 5615b1e1ec | |||
|
|
9d66a5ed4f | ||
|
|
5555f1afec | ||
|
|
77b1b895a5 | ||
|
|
6751bc15cc | ||
|
|
735fa97089 | ||
|
|
314774f032 | ||
|
|
262bbf26ea | ||
|
|
cdf8d043c9 | ||
|
|
1eb7c94a5a | ||
|
|
5228d8cf4d | ||
|
|
782779f0d7 | ||
|
|
18ec8e6b6c | ||
|
|
7d82d8b76f | ||
|
|
f957756df7 | ||
|
|
2cb0553723 | ||
|
|
8464e151cc | ||
|
|
2da444e7c2 | ||
|
|
f92509fddf | ||
|
|
62772615b6 | ||
|
|
8c5b0ed5c4 | ||
|
|
8481ab85de | ||
|
|
881d3a3aa1 | ||
|
|
878509090f | ||
|
|
24657ecc75 | ||
|
|
881ece214d | ||
|
|
2519b0ee9f | ||
|
|
ba1589e2e2 | ||
|
|
e9a2473bad |
36
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
## Summary
|
||||
|
||||
_[Please provide a summary of the changes in this PR.]_
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I have read (or I am familiar with) the [Contribution Guidelines](../docs/CONTRIBUTING.md)
|
||||
- [ ] I have tested the changes in this PR
|
||||
- [ ] My PR is either small, or I have split it into smaller logical commits that are easier to review
|
||||
- [ ] I have added the signoff line to all my commits. See [Signing off your work](../docs/CONTRIBUTING.md#sign-your-work---the-developers-certificate-of-origin)
|
||||
- [ ] I have added appropriate changelog entries for the changes in this PR. See [Adding changelog entries](../docs/CONTRIBUTING.md#add-changelog-changed-changelog-fixed-etc)
|
||||
- [ ] I do not need to add a changelog entry. Reason: _[Please provide a reason]_
|
||||
- [ ] I have added appropriate `Closes:` or `Fixes:` tags in the commit messages wherever applicable, or made sure those are not needed. See [Submitting patches](https://github.com/damus-io/damus/blob/master/docs/CONTRIBUTING.md#submitting-patches)
|
||||
|
||||
## Test report
|
||||
|
||||
_Please provide a test report for the changes in this PR. You can use the template below, but feel free to modify it as needed._
|
||||
|
||||
**Device:** _[Please specify the device you used for testing]_
|
||||
|
||||
**iOS:** _[Please specify the iOS version you used for testing]_
|
||||
|
||||
**Damus:** _[Please specify the Damus version or commit hash you used for testing]_
|
||||
|
||||
**Setup:** _[Please provide a brief description of the setup you used for testing, if applicable]_
|
||||
|
||||
**Steps:** _[Please provide a list of steps you took to test the changes in this PR]_
|
||||
|
||||
**Results:**
|
||||
- [ ] PASS
|
||||
- [ ] Partial PASS
|
||||
- Details: _[Please provide details of the partial pass]_
|
||||
|
||||
## Other notes
|
||||
|
||||
_[Please provide any other information that you think is relevant to this PR.]_
|
||||
104
CHANGELOG.md
@@ -1,3 +1,107 @@
|
||||
## [v1.11(10)](https://github.com/damus-io/damus/releases/tag/v1.11-10) - 2024-11-18
|
||||
|
||||
### Added
|
||||
|
||||
- Add Damus Share Feature (Swift)
|
||||
- Added new easy to use video controls for full screen video (Daniel D’Aquino)
|
||||
- Add Edit, Share, and Tap-gesture in Profile pic image viewer (Swift Coder)
|
||||
- Disappearing header, tabbar, and post button on scroll (ericholguin)
|
||||
- Add Apple translation popovers for notes for iOS 17.4+ and macOS 14.4+ (Terry Yiu)
|
||||
- Added NDB search functionality to the universe view (ericholguin)
|
||||
- Added mute button to ProfileActionSheet (chungwwei)
|
||||
- Added mute action to selected text menu (ericholguin)
|
||||
- Added support for pasting images from the clipboard to the post composer (Swift Coder)
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved image carousel image fill behavior (Daniel D’Aquino)
|
||||
- Improved video syncing and bandwidth usage when switching between timeline video and full screen mode (Daniel D’Aquino)
|
||||
- Swipe to dismiss on full screen carousel now shows an opacity effect for improved UX (Daniel D’Aquino)
|
||||
- Removed event contents from full screen media carousel for cleaner view (Daniel D’Aquino)
|
||||
- Add share button for images on full screen image carousel view (Swift)
|
||||
- Changed boldness of font in side menu labels. (ericholguin)
|
||||
- Changed search notes button with searched keyword (ericholguin)
|
||||
- Changed opacity of tabbar and post button (ericholguin)
|
||||
- Allow multiple images to be uploaded at the same time (swiftcoder) (William Casarin)
|
||||
- Changed side menu design (ericholguin)
|
||||
- Truncate fulltext search results (William Casarin)
|
||||
- Expanded profile search results to 128 (William Casarin)
|
||||
- Expand nostrdb text search results to 128 items (William Casarin)
|
||||
- Use LazyVStack in text search results (William Casarin)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed missing tab bar on navigation (Swift Coder)
|
||||
- Fixed some issues where QR code would not work, and improved UX (Daniel D’Aquino)
|
||||
- Fixed iOS 18 gesture issues that would take user to the thread view when clicking on a video or unmuting it (Daniel D’Aquino)
|
||||
- Fixed several issues that would cause video to automatically play or pause incorrectly (Daniel D’Aquino)
|
||||
- Fixed issue where full screen video would disappear when going to landscape mode (Daniel D’Aquino)
|
||||
- Fixed portrait video size on full screen carousel (Daniel D’Aquino)
|
||||
- Fix avatar image on qrcode view (Swift Coder)
|
||||
- Fix banner image upload (Swift Coder)
|
||||
- Fix dismiss button visibility (Swift Coder)
|
||||
- Fix quote repost counting (William Casarin)
|
||||
- Fixed overlapping text in Universe View (ericholguin)
|
||||
- Fixed localization issues and exported strings (Terry Yiu)
|
||||
- Fix sensitive long-press gesture on event chat bubble in iOS 18 (Daniel D’Aquino)
|
||||
- Fixed bottom padding for tabbar (ericholguin)
|
||||
- Fixed localization build failures (Terry Yiu)
|
||||
- Fixed back nav button placement in profile edit view (ericholguin)
|
||||
- Friend profiles will now more likely show up in profile search (William Casarin)
|
||||
- Fix broken QR code scanner and fix landscape mode (Terry Yiu)
|
||||
|
||||
[1.11(10)](https://github.com/damus-io/damus/releases/tag/v1.11-10): https://github.com/damus-io/damus/releases/tag/v1.11-10
|
||||
|
||||
## [1.10.1] - 2024-09-22
|
||||
|
||||
### Added
|
||||
|
||||
- Push notification support (Daniel D’Aquino)
|
||||
- Added profile edit safe guards (Eric Holguin)
|
||||
- Tor relay icon (ericholguin)
|
||||
- Add highlighter for web pages (Daniel D’Aquino)
|
||||
- Add support for adding comments when creating a highlight (Daniel D’Aquino)
|
||||
- Add support for rendering highlights with comments (Daniel D’Aquino)
|
||||
- Ability to create highlights (ericholguin)
|
||||
- Highlights (NIP-84) (ericholguin)
|
||||
- Revamp emoji picker to be less error-prone and add search, frequently used, and multiple skin tone support capabilities (Terry Yiu)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve notification view filtering UX (Daniel D’Aquino)
|
||||
- Improve visibility of friends filter button (Daniel D’Aquino)
|
||||
- Changed the default banner from ostriches to damoose (Eric Holguin)
|
||||
- Changed image and banner url text fields to new sheet view (Eric Holguin)
|
||||
- Onboarding design (ericholguin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix items that became unclickable on iOS 18 (Daniel D’Aquino)
|
||||
- Fix many reconnection issues (William Casarin)
|
||||
- Fixed issue where theme would be changed to black and can't be switched back on iOS 18 (cr0bar)
|
||||
- Fixed some scenarios where the contact list would never be saved locally and cause issues when switching relays. (Daniel D’Aquino)
|
||||
- Fix albyhub zaps not appearing (William Casarin)
|
||||
- Fix inadvertent escape from mention suggestion menu when typing a space character (Daniel D’Aquino)
|
||||
- Fix profile view toolbar alignment bug in iOS 18 (Terry Yiu)
|
||||
- Create Account model now uses correct metadata (ericholguin)
|
||||
- Restore localization for custom tabs (William Casarin)
|
||||
- Fix iOS 18 reflection runtime error for custom picker (William Casarin)
|
||||
|
||||
|
||||
[1.10.1]: https://github.com/damus-io/damus/releases/tag/v1.10.1
|
||||
|
||||
|
||||
## [1.9.1 (4)] - 2024-08-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix crash when viewing notes with invalid image dimension metadata (Daniel D’Aquino)
|
||||
|
||||
[1.9.1 (4)]: https://github.com/damus-io/damus/releases/tag/v1.9.1-4
|
||||
|
||||
|
||||
## [1.9 (14)] - 2024-07-14
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
{
|
||||
"originHash" : "babaf4d5748afecf49bbb702530d8e9576460692f478b0a50ee43195dd4440e2",
|
||||
"originHash" : "0d806129a33991730dd1aa3d38c47a745f9e9e6ff44999f4a7f28b695f024832",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "codescanner",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/twostraws/CodeScanner.git",
|
||||
"state" : {
|
||||
"revision" : "9fa582f4b36c69c2a55bff5fb3377eb170ae273c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "emojikit",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -89,13 +97,20 @@
|
||||
"version" : "0.1.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftycrop",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/benedom/SwiftyCrop",
|
||||
"state" : {
|
||||
"revision" : "454d0a0d4faf6f3a19c8d817ab9d7d27524bd79f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swipeactions",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/aheze/SwipeActions",
|
||||
"location" : "https://github.com/damus-io/SwipeActions.git",
|
||||
"state" : {
|
||||
"revision" : "41e6f6dce02d8cfa164f8c5461a41340850ca3ab",
|
||||
"version" : "1.1.0"
|
||||
"revision" : "33d99756c3112e1a07c1732e3cddc5ad5bd0c5f4"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "YES">
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "4CE6DEFC27F7A08200C66700"
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 511 KiB After Width: | Height: | Size: 511 KiB |
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 547 KiB After Width: | Height: | Size: 547 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
6
damus/Assets.xcassets/Logos/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
23
damus/Assets.xcassets/Logos/alby.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "alby.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "alby.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "alby.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 300 KiB After Width: | Height: | Size: 300 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "profile-banner.jpeg",
|
||||
"filename" : "coinos.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
BIN
damus/Assets.xcassets/Logos/coinos.imageset/coinos.png
vendored
Normal file
|
After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 290 KiB |
12
damus/Assets.xcassets/alby-go.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "alby-go.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
damus/Assets.xcassets/alby-go.imageset/alby-go.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "alby.svg",
|
||||
"idiom": "universal",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"scale": "2x",
|
||||
"filename": "alby.svg"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"scale": "3x",
|
||||
"filename": "alby.svg"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,6 @@ struct CustomPicker<SelectionValue: Hashable>: View {
|
||||
.accentColor(tag == selection ? textColor() : .gray)
|
||||
}
|
||||
}
|
||||
.background(Color(UIColor.systemBackground))
|
||||
}
|
||||
|
||||
func textColor() -> Color {
|
||||
|
||||
@@ -20,6 +20,7 @@ struct DamusBackground: View {
|
||||
.resizable()
|
||||
.frame(maxWidth: .infinity, maxHeight: maxHeight, alignment: .center)
|
||||
.ignoresSafeArea()
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// MutinyGradient.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 3/9/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
fileprivate let mutiny_grad_c1 = hex_col(r: 39, g: 95, b: 161)
|
||||
fileprivate let mutiny_grad_c2 = hex_col(r: 13, g: 33, b: 56)
|
||||
fileprivate let mutiny_grad = [mutiny_grad_c2, mutiny_grad_c1]
|
||||
|
||||
let MutinyGradient: LinearGradient =
|
||||
LinearGradient(colors: mutiny_grad, startPoint: .top, endPoint: .bottom)
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import Combine
|
||||
|
||||
// TODO: all this ShareSheet complexity can be replaced with ShareLink once we update to iOS 16
|
||||
struct ShareSheet: UIViewControllerRepresentable {
|
||||
@@ -95,64 +96,203 @@ enum ImageShape {
|
||||
}
|
||||
}
|
||||
|
||||
/// The `CarouselModel` helps `ImageCarousel` with some state management logic, keeping track of media sizes, and the ideal display size
|
||||
///
|
||||
/// This model is necessary because the state management logic required to keep track of media sizes for each one of the carousel items,
|
||||
/// and the ideal display size at each moment is not a trivial task.
|
||||
///
|
||||
/// The rules for the media fill are as follows:
|
||||
/// 1. The media item should generally have a width that completely fills the width of its parent view
|
||||
/// 2. The height of the carousel should be adjusted accordingly
|
||||
/// 3. The only exception to rules 1 and 2 is when the total height would be 20% larger than the height of the device
|
||||
/// 4. If none of the above can be computed (e.g. due to missing information), default to a reasonable height, where the media item will fit into.
|
||||
///
|
||||
/// ## Usage notes
|
||||
///
|
||||
/// The view is has the following state management responsibilities:
|
||||
/// 1. Watching the size of the images (via the `.observe_image_size` modifier)
|
||||
/// 2. Notifying this class of geometry reader changes, by setting `geo_size`
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// This class is organized in a way to reduce stateful behavior and the transiency bugs it can cause.
|
||||
///
|
||||
/// This is accomplished through the following pattern:
|
||||
/// 1. The `current_item_fill` is a published property so that any updates instantly re-render the view
|
||||
/// 2. However, `current_item_fill` has a mathematical dependency on other members of this class
|
||||
/// 3. Therefore, the members on which the fill property depends on all have `didSet` observers that will cause the `current_item_fill` to be recalculated and published.
|
||||
///
|
||||
/// This pattern helps ensure that the state is always consistent and that the view is always up-to-date.
|
||||
///
|
||||
/// This class is marked as `@MainActor` since most of its properties are published and should be accessed from the main thread to avoid inconsistent SwiftUI state during renders
|
||||
@MainActor
|
||||
class CarouselModel: ObservableObject {
|
||||
var current_url: URL?
|
||||
var fillHeight: CGFloat
|
||||
var maxHeight: CGFloat
|
||||
var firstImageHeight: CGFloat?
|
||||
// MARK: Immutable object attributes
|
||||
// These are some attributes that are not expected to change throughout the lifecycle of this object
|
||||
// These should not be modified after initialization to avoid state inconsistency
|
||||
|
||||
/// The state of the app
|
||||
let damus_state: DamusState
|
||||
/// All urls in the carousel
|
||||
let urls: [MediaUrl]
|
||||
/// The default fill height for the carousel, if we cannot calculate a more appropriate height
|
||||
/// **Usage note:** Default to this when `current_item_fill` is nil
|
||||
let default_fill_height: CGFloat
|
||||
/// The maximum height for any carousel item
|
||||
let max_height: CGFloat
|
||||
|
||||
|
||||
// MARK: Miscellaneous
|
||||
|
||||
/// Holds items that allows us to cancel video size observers during de-initialization
|
||||
private var all_cancellables: [AnyCancellable] = []
|
||||
|
||||
|
||||
// MARK: State management properties
|
||||
/// Properties relevant to state management.
|
||||
/// These should be made into computed/functional properties when possible to avoid stateful behavior
|
||||
/// When that is not possible (e.g. when dealing with an observed published property), establish its mathematical dependencies,
|
||||
/// and use `didSet` observers to ensure that the state is always re-computed when necessary.
|
||||
|
||||
@Published var open_sheet: Bool
|
||||
@Published var selectedIndex: Int
|
||||
@Published var video_size: CGSize?
|
||||
@Published var image_fill: ImageFill?
|
||||
/// Stores information about the size of each media item in `urls`.
|
||||
/// **Usage note:** The view is responsible for setting the size of image urls
|
||||
var media_size_information: [URL: CGSize] {
|
||||
didSet {
|
||||
guard let current_url else { return }
|
||||
// Upon updating information, update the carousel fill size if the size for the current url has changed
|
||||
if oldValue[current_url] != media_size_information[current_url] {
|
||||
self.refresh_current_item_fill()
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Stores information about the geometry reader
|
||||
/// **Usage note:** The view is responsible for setting this value
|
||||
var geo_size: CGSize? {
|
||||
didSet { self.refresh_current_item_fill() }
|
||||
}
|
||||
/// The index of the currently selected item
|
||||
/// **Usage note:** The view is responsible for setting this value
|
||||
@Published var selectedIndex: Int {
|
||||
didSet { self.refresh_current_item_fill() }
|
||||
}
|
||||
/// The current fill for the media item.
|
||||
/// **Usage note:** This property is read-only and should not be set directly. Update `selectedIndex` to update the current item being viewed.
|
||||
var current_url: URL? {
|
||||
return urls[safe: selectedIndex]?.url
|
||||
}
|
||||
/// Holds the ideal fill dimensions for the current item.
|
||||
/// **Usage note:** This property is automatically updated when other properties are set, and should not be set directly
|
||||
/// **Implementation note:** This property is mathematically dependent on geo_size, media_size_information, and `selectedIndex`,
|
||||
/// and is automatically updated upon changes to these properties.
|
||||
@Published private(set) var current_item_fill: ImageFill?
|
||||
|
||||
|
||||
// MARK: Initialization and de-initialization
|
||||
|
||||
init(image_fill: ImageFill?) {
|
||||
self.current_url = nil
|
||||
self.fillHeight = 350
|
||||
self.maxHeight = UIScreen.main.bounds.height * 1.2 // 1.2
|
||||
self.firstImageHeight = nil
|
||||
self.open_sheet = false
|
||||
/// Initializes the `CarouselModel` with the given `DamusState` and `MediaUrl` array
|
||||
init(damus_state: DamusState, urls: [MediaUrl]) {
|
||||
// Immutable object attributes
|
||||
self.damus_state = damus_state
|
||||
self.urls = urls
|
||||
self.default_fill_height = 350
|
||||
self.max_height = UIScreen.main.bounds.height * 1.2 // 1.2
|
||||
|
||||
// State management properties
|
||||
self.selectedIndex = 0
|
||||
self.video_size = nil
|
||||
self.image_fill = image_fill
|
||||
self.current_item_fill = nil
|
||||
self.geo_size = nil
|
||||
self.media_size_information = [:]
|
||||
|
||||
// Setup the rest of the state management logic
|
||||
self.observe_video_sizes()
|
||||
Task {
|
||||
self.refresh_current_item_fill()
|
||||
}
|
||||
}
|
||||
|
||||
/// This private function observes the video sizes for all videos
|
||||
private func observe_video_sizes() {
|
||||
for media_url in urls {
|
||||
switch media_url {
|
||||
case .video(let url):
|
||||
let video_player = damus_state.video.get_player(for: url)
|
||||
if let video_size = video_player.video_size {
|
||||
self.media_size_information[url] = video_size // Set the initial size if available
|
||||
}
|
||||
let observer_cancellable = video_player.$video_size.sink(receiveValue: { new_size in
|
||||
self.media_size_information[url] = new_size // Update the size when it changes
|
||||
})
|
||||
all_cancellables.append(observer_cancellable) // Store the cancellable to cancel it later
|
||||
case .image(_):
|
||||
break; // Observing an image size needs to be done on the view directly, through the `.observe_image_size` modifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
for cancellable_item in all_cancellables {
|
||||
cancellable_item.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: State management and logic
|
||||
|
||||
/// This function refreshes the current item fill based on the current state of the model
|
||||
/// **Usage note:** This is private, do not call this directly from outside the class.
|
||||
/// **Implementation note:** This should be called using `didSet` observers on properties that affect the fill
|
||||
private func refresh_current_item_fill() {
|
||||
if let current_url,
|
||||
let item_size = self.media_size_information[current_url],
|
||||
let geo_size {
|
||||
self.current_item_fill = ImageFill.calculate_image_fill(
|
||||
geo_size: geo_size,
|
||||
img_size: item_size,
|
||||
maxHeight: self.max_height,
|
||||
fillHeight: self.default_fill_height
|
||||
)
|
||||
}
|
||||
else {
|
||||
self.current_item_fill = nil // Not enough information to compute the proper fill. Default to nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image Carousel
|
||||
|
||||
/// A carousel that displays images and videos
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - State management logic is mostly handled by `CarouselModel`, as it is complex, and becomes difficult to manage in a view
|
||||
///
|
||||
@MainActor
|
||||
struct ImageCarousel<Content: View>: View {
|
||||
var urls: [MediaUrl]
|
||||
|
||||
/// The event id of the note that this carousel is displaying
|
||||
let evid: NoteId
|
||||
|
||||
let state: DamusState
|
||||
/// The model that holds information and state of this carousel
|
||||
/// This is observed to update the view when the model changes
|
||||
@ObservedObject var model: CarouselModel
|
||||
let content: ((_ dismiss: @escaping (() -> Void)) -> Content)?
|
||||
|
||||
init(state: DamusState, evid: NoteId, urls: [MediaUrl]) {
|
||||
self.urls = urls
|
||||
self.evid = evid
|
||||
self.state = state
|
||||
let media_model = state.events.get_cache_data(evid).media_metadata_model
|
||||
self._model = ObservedObject(initialValue: CarouselModel(image_fill: media_model.fill))
|
||||
self._model = ObservedObject(initialValue: CarouselModel(damus_state: state, urls: urls))
|
||||
self.content = nil
|
||||
}
|
||||
|
||||
init(state: DamusState, evid: NoteId, urls: [MediaUrl], @ViewBuilder content: @escaping (_ dismiss: @escaping (() -> Void)) -> Content) {
|
||||
self.urls = urls
|
||||
self.evid = evid
|
||||
self.state = state
|
||||
let media_model = state.events.get_cache_data(evid).media_metadata_model
|
||||
self._model = ObservedObject(initialValue: CarouselModel(image_fill: media_model.fill))
|
||||
self._model = ObservedObject(initialValue: CarouselModel(damus_state: state, urls: urls))
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var filling: Bool {
|
||||
model.image_fill?.filling == true
|
||||
model.current_item_fill?.filling == true
|
||||
}
|
||||
|
||||
var height: CGFloat {
|
||||
model.firstImageHeight ?? model.image_fill?.height ?? model.fillHeight
|
||||
// Use the calculated fill height if available, otherwise use the default fill height
|
||||
model.current_item_fill?.height ?? model.default_fill_height
|
||||
}
|
||||
|
||||
func Placeholder(url: URL, geo_size: CGSize, num_urls: Int) -> some View {
|
||||
@@ -160,7 +300,7 @@ struct ImageCarousel<Content: View>: View {
|
||||
if num_urls > 1 {
|
||||
// jb55: quick hack since carousel with multiple images looks horrible with blurhash background
|
||||
Color.clear
|
||||
} else if let meta = state.events.lookup_img_metadata(url: url),
|
||||
} else if let meta = model.damus_state.events.lookup_img_metadata(url: url),
|
||||
case .processed(let blurhash) = meta.state {
|
||||
Image(uiImage: blurhash)
|
||||
.resizable()
|
||||
@@ -169,12 +309,6 @@ struct ImageCarousel<Content: View>: View {
|
||||
Color.clear
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if self.model.image_fill == nil, let size = state.video.size_for_url(url) {
|
||||
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: size, maxHeight: model.maxHeight, fillHeight: model.fillHeight)
|
||||
self.model.image_fill = fill
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Media(geo: GeometryProxy, url: MediaUrl, index: Int) -> some View {
|
||||
@@ -183,24 +317,17 @@ struct ImageCarousel<Content: View>: View {
|
||||
case .image(let url):
|
||||
Img(geo: geo, url: url, index: index)
|
||||
.onTapGesture {
|
||||
model.open_sheet = true
|
||||
present(full_screen_item: .full_screen_carousel(urls: model.urls, selectedIndex: $model.selectedIndex))
|
||||
}
|
||||
case .video(let url):
|
||||
DamusVideoPlayer(url: url, video_size: $model.video_size, controller: state.video, style: .preview(on_tap: { model.open_sheet = true }))
|
||||
.onChange(of: model.video_size) { size in
|
||||
guard let size else { return }
|
||||
|
||||
let fill = ImageFill.calculate_image_fill(geo_size: geo.size, img_size: size, maxHeight: model.maxHeight, fillHeight: model.fillHeight)
|
||||
|
||||
print("video_size changed \(size)")
|
||||
if self.model.image_fill == nil {
|
||||
print("video_size firstImageHeight \(fill.height)")
|
||||
self.model.firstImageHeight = fill.height
|
||||
state.events.get_cache_data(evid).media_metadata_model.fill = fill
|
||||
}
|
||||
|
||||
self.model.image_fill = fill
|
||||
}
|
||||
let video_model = model.damus_state.video.get_player(for: url)
|
||||
DamusVideoPlayerView(
|
||||
model: video_model,
|
||||
coordinator: model.damus_state.video,
|
||||
style: .preview(on_tap: {
|
||||
present(full_screen_item: .full_screen_carousel(urls: model.urls, selectedIndex: $model.selectedIndex))
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,31 +336,18 @@ struct ImageCarousel<Content: View>: View {
|
||||
KFAnimatedImage(url)
|
||||
.callbackQueue(.dispatch(.global(qos:.background)))
|
||||
.backgroundDecode(true)
|
||||
.imageContext(.note, disable_animation: state.settings.disable_animation)
|
||||
.imageContext(.note, disable_animation: model.damus_state.settings.disable_animation)
|
||||
.image_fade(duration: 0.25)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.imageFill(for: geo.size, max: model.maxHeight, fill: model.fillHeight) { fill in
|
||||
state.events.get_cache_data(evid).media_metadata_model.fill = fill
|
||||
// blur hash can be discarded when we have the url
|
||||
// NOTE: this is the wrong place for this... we need to remove
|
||||
// it when the image is loaded in memory. This may happen
|
||||
// earlier than this (by the preloader, etc)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
state.events.lookup_img_metadata(url: url)?.state = .not_needed
|
||||
}
|
||||
self.model.image_fill = fill
|
||||
if index == 0 {
|
||||
self.model.firstImageHeight = fill.height
|
||||
//maxHeight = firstImageHeight ?? maxHeight
|
||||
} else {
|
||||
//maxHeight = firstImageHeight ?? fill.height
|
||||
}
|
||||
}
|
||||
.observe_image_size(size_changed: { size in
|
||||
// Observe the image size to update the model when the size changes, so we can calculate the fill
|
||||
model.media_size_information[url] = size
|
||||
})
|
||||
.background {
|
||||
Placeholder(url: url, geo_size: geo.size, num_urls: urls.count)
|
||||
Placeholder(url: url, geo_size: geo.size, num_urls: model.urls.count)
|
||||
}
|
||||
.aspectRatio(contentMode: filling ? .fill : .fit)
|
||||
.kfClickable()
|
||||
@@ -248,25 +362,19 @@ struct ImageCarousel<Content: View>: View {
|
||||
|
||||
var Medias: some View {
|
||||
TabView(selection: $model.selectedIndex) {
|
||||
ForEach(urls.indices, id: \.self) { index in
|
||||
ForEach(model.urls.indices, id: \.self) { index in
|
||||
GeometryReader { geo in
|
||||
Media(geo: geo, url: urls[index], index: index)
|
||||
Media(geo: geo, url: model.urls[index], index: index)
|
||||
.onChange(of: geo.size, perform: { new_size in
|
||||
model.geo_size = new_size
|
||||
})
|
||||
.onAppear {
|
||||
model.geo_size = geo.size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||
.fullScreenCover(isPresented: $model.open_sheet) {
|
||||
if let content {
|
||||
FullScreenCarouselView<Content>(video_controller: state.video, urls: urls, settings: state.settings, selectedIndex: $model.selectedIndex) {
|
||||
content({ // Dismiss closure
|
||||
model.open_sheet = false
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
FullScreenCarouselView<AnyView>(video_controller: state.video, urls: urls, settings: state.settings, selectedIndex: $model.selectedIndex)
|
||||
}
|
||||
}
|
||||
.frame(height: height)
|
||||
.onChange(of: model.selectedIndex) { value in
|
||||
model.selectedIndex = value
|
||||
@@ -284,8 +392,8 @@ struct ImageCarousel<Content: View>: View {
|
||||
}
|
||||
|
||||
|
||||
if urls.count > 1 {
|
||||
PageControlView(currentPage: $model.selectedIndex, numberOfPages: urls.count)
|
||||
if model.urls.count > 1 {
|
||||
PageControlView(currentPage: $model.selectedIndex, numberOfPages: model.urls.count)
|
||||
.frame(maxWidth: 0, maxHeight: 0)
|
||||
.padding(.top, 5)
|
||||
}
|
||||
@@ -293,27 +401,6 @@ struct ImageCarousel<Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image Modifier
|
||||
extension KFOptionSetter {
|
||||
/// Sets a block to get image size
|
||||
///
|
||||
/// - Parameter block: The block which is used to read the image object.
|
||||
/// - Returns: `Self` value after read size
|
||||
public func imageFill(for size: CGSize, max: CGFloat, fill: CGFloat, block: @escaping (ImageFill) throws -> Void) -> Self {
|
||||
let modifier = AnyImageModifier { image -> KFCrossPlatformImage in
|
||||
let img_size = image.size
|
||||
let geo_size = size
|
||||
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: img_size, maxHeight: max, fillHeight: fill)
|
||||
DispatchQueue.main.async { [block, fill] in
|
||||
try? block(fill)
|
||||
}
|
||||
return image
|
||||
}
|
||||
options.imageModifier = modifier
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct ImageFill {
|
||||
let filling: Bool?
|
||||
@@ -350,4 +437,3 @@ struct ImageCarousel_Previews: PreviewProvider {
|
||||
.environmentObject(OrientationTracker())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
//
|
||||
// OfflineTranslateView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 9/29/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import SwiftUI
|
||||
import NaturalLanguage
|
||||
import Translation
|
||||
|
||||
fileprivate let MIN_UNIQUE_CHARS = 2
|
||||
|
||||
@available(iOS 18.0, macOS 15.0, *)
|
||||
@available(macCatalyst, unavailable)
|
||||
struct OfflineTranslateView: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let size: EventViewKind
|
||||
|
||||
@ObservedObject var translations_model: TranslationModel
|
||||
|
||||
@State private var translationConfiguration: TranslationSession.Configuration?
|
||||
|
||||
// @State private var languageStatus: LanguageAvailability.Status?
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.size = size
|
||||
self._translations_model = ObservedObject(wrappedValue: damus_state.events.get_cache_data(event.id).translations_model)
|
||||
}
|
||||
|
||||
var TranslateButton: some View {
|
||||
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
|
||||
translate()
|
||||
}
|
||||
.translate_button_style()
|
||||
}
|
||||
|
||||
func TranslatedView(lang: String?, artifacts: NoteArtifactsSeparated, font_size: Double) -> some View {
|
||||
return VStack(alignment: .leading) {
|
||||
let translatedFromLanguageString = String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang ?? "ja")
|
||||
Text(translatedFromLanguageString)
|
||||
.foregroundColor(.gray)
|
||||
.font(.footnote)
|
||||
.padding([.top, .bottom], 10)
|
||||
|
||||
if self.size == .selected {
|
||||
SelectableText(damus_state: damus_state, event: event, attributedString: artifacts.content.attributed, size: self.size)
|
||||
} else {
|
||||
artifacts.content.text
|
||||
.font(eventviewsize_to_font(self.size, font_size: font_size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func translate() {
|
||||
guard /*let languageStatus, */translations_model.state == .havent_tried && damus_state.settings.translation_service == .none && damus_state.settings.translate_offline/* && languageStatus != .unsupported*/, let note_language = translations_model.note_language else {
|
||||
return
|
||||
}
|
||||
|
||||
guard translationConfiguration == nil else {
|
||||
translationConfiguration?.invalidate()
|
||||
return
|
||||
}
|
||||
|
||||
translationConfiguration = TranslationSession.Configuration(
|
||||
source: Locale.Language(identifier: note_language))
|
||||
}
|
||||
|
||||
// func setLanguageStatus() async {
|
||||
// guard languageStatus == nil else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// guard let note_language = translations_model.note_language else {
|
||||
// languageStatus = .unsupported
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let languageAvailability = LanguageAvailability()
|
||||
// let language = Locale.Language(identifier: note_language)
|
||||
// languageStatus = await languageAvailability.status(from: language, to: nil)
|
||||
// }
|
||||
|
||||
var body: some View {
|
||||
if let note_lang = translations_model.note_language, damus_state.settings.translation_service == .none && damus_state.settings.translate_offline && should_translate(event: event, our_keypair: damus_state.keypair, note_lang: note_lang) {
|
||||
Group {
|
||||
switch self.translations_model.state {
|
||||
case .havent_tried:
|
||||
if damus_state.settings.auto_translate/* && languageStatus == .installed*/ {
|
||||
Text("")
|
||||
} else {
|
||||
TranslateButton
|
||||
}
|
||||
case .translating:
|
||||
Text("")
|
||||
case .translated(let translated):
|
||||
let languageName = Locale.current.localizedString(forLanguageCode: translated.language)
|
||||
TranslatedView(lang: languageName, artifacts: translated.artifacts, font_size: damus_state.settings.font_size)
|
||||
case .not_needed:
|
||||
Text("")
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// Task { @MainActor in
|
||||
// await setLanguageStatus()
|
||||
// }
|
||||
translate()
|
||||
}
|
||||
.translationTask(translationConfiguration) { translationSession in
|
||||
Task { @MainActor in
|
||||
do {
|
||||
guard let note_language = translations_model.note_language, translations_model.state == .havent_tried/*, languageStatus != .unsupported*/ else {
|
||||
return
|
||||
}
|
||||
|
||||
translations_model.state = .translating
|
||||
|
||||
let originalContent = event.get_content(damus_state.keypair)
|
||||
let response = try await translationSession.translate(originalContent)
|
||||
let translated_note = response.targetText
|
||||
|
||||
guard originalContent != translated_note else {
|
||||
// if its the same, give up and don't retry
|
||||
translations_model.state = .not_needed
|
||||
return
|
||||
}
|
||||
|
||||
guard translationMeetsStringDistanceRequirements(original: originalContent, translated: translated_note) else {
|
||||
translations_model.state = .not_needed
|
||||
return
|
||||
}
|
||||
|
||||
// Render translated note
|
||||
let translated_blocks = parse_note_content(content: .content(translated_note, event.tags))
|
||||
let artifacts = render_blocks(blocks: translated_blocks, profiles: damus_state.profiles)
|
||||
|
||||
// and cache it
|
||||
translations_model.state = .translated(Translated(artifacts: artifacts, language: note_language))
|
||||
} catch {
|
||||
// code to handle error
|
||||
print("Error translating note: \(error.localizedDescription)")
|
||||
translations_model.state = .not_needed
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("")
|
||||
}
|
||||
}
|
||||
|
||||
func translationMeetsStringDistanceRequirements(original: String, translated: String) -> Bool {
|
||||
return levenshteinDistanceIsGreaterThanOrEqualTo(from: original, to: translated, threshold: MIN_UNIQUE_CHARS)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 18.0, macOS 15.0, *)
|
||||
@available(macCatalyst, unavailable)
|
||||
struct OfflineTranslateView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let ds = test_damus_state
|
||||
OfflineTranslateView(damus_state: ds, event: test_note, size: .normal)
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ struct TranslateView: View {
|
||||
return false
|
||||
}
|
||||
|
||||
if TranslationService.isAppleTranslationSupported {
|
||||
if TranslationService.isAppleTranslationPopoverSupported {
|
||||
return damus_state.settings.translation_service == .none || damus_state.settings.can_translate
|
||||
} else {
|
||||
return damus_state.settings.can_translate
|
||||
|
||||
@@ -57,10 +57,47 @@ enum Sheets: Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
/// An item to be presented full screen in a mechanism that is more robust for timeline views.
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// This is part of the `present(full_screen_item: FullScreenItem)` interface that allows views in a timeline to show something full-screen without the lazy stack issues
|
||||
/// Full screen cover modifiers are not suitable in those cases because device orientation changes or programmatic scroll commands will cause the view to be unloaded along with the cover,
|
||||
/// causing the user to lose the full screen view randomly.
|
||||
///
|
||||
/// The `ContentView` is responsible for handling these objects
|
||||
///
|
||||
/// New items can be added as needed.
|
||||
///
|
||||
enum FullScreenItem: Identifiable, Equatable {
|
||||
/// A full screen media carousel for images and videos.
|
||||
case full_screen_carousel(urls: [MediaUrl], selectedIndex: Binding<Int>)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .full_screen_carousel(let urls, _): return "full_screen_carousel:\(urls.map(\.url))"
|
||||
}
|
||||
}
|
||||
|
||||
static func == (lhs: FullScreenItem, rhs: FullScreenItem) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
|
||||
/// The view to display the item
|
||||
func view(damus_state: DamusState) -> some View {
|
||||
switch self {
|
||||
case .full_screen_carousel(let urls, let selectedIndex):
|
||||
return FullScreenCarouselView<AnyView>(video_coordinator: damus_state.video, urls: urls, settings: damus_state.settings, selectedIndex: selectedIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func present_sheet(_ sheet: Sheets) {
|
||||
notify(.present_sheet(sheet))
|
||||
}
|
||||
|
||||
var tabHeight: CGFloat = 0.0
|
||||
|
||||
struct ContentView: View {
|
||||
let keypair: Keypair
|
||||
let appDelegate: AppDelegate?
|
||||
@@ -76,6 +113,7 @@ struct ContentView: View {
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
|
||||
@State var active_sheet: Sheets? = nil
|
||||
@State var active_full_screen_item: FullScreenItem? = nil
|
||||
@State var damus_state: DamusState!
|
||||
@State var menu_subtitle: String? = nil
|
||||
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home {
|
||||
@@ -89,6 +127,7 @@ struct ContentView: View {
|
||||
@State var user_muted_confirm: Bool = false
|
||||
@State var confirm_overwrite_mutelist: Bool = false
|
||||
@State private var isSideBarOpened = false
|
||||
@State var headerOffset: CGFloat = 0.0
|
||||
var home: HomeModel = HomeModel()
|
||||
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
||||
@AppStorage("has_seen_suggested_users") private var hasSeenOnboardingSuggestions = false
|
||||
@@ -131,7 +170,7 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
case .home:
|
||||
PostingTimelineView(damus_state: damus_state!, home: home, active_sheet: $active_sheet)
|
||||
PostingTimelineView(damus_state: damus_state!, home: home, isSideBarOpened: $isSideBarOpened, active_sheet: $active_sheet, headerOffset: $headerOffset)
|
||||
|
||||
case .notifications:
|
||||
NotificationsView(state: damus, notifications: home.notifications, subtitle: $menu_subtitle)
|
||||
@@ -140,25 +179,16 @@ struct ContentView: View {
|
||||
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings)
|
||||
}
|
||||
}
|
||||
.background(DamusColors.adaptableWhite)
|
||||
.edgesIgnoringSafeArea(selected_timeline != .home ? [] : [.top, .bottom])
|
||||
.navigationBarTitle(timeline_name(selected_timeline), displayMode: .inline)
|
||||
.toolbar(selected_timeline != .home ? .visible : .hidden)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
VStack {
|
||||
if selected_timeline == .home {
|
||||
Image("damus-home")
|
||||
.resizable()
|
||||
.frame(width:30,height:30)
|
||||
.shadow(color: DamusColors.purple, radius: 2)
|
||||
.opacity(isSideBarOpened ? 0 : 1)
|
||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||
.onTapGesture {
|
||||
isSideBarOpened.toggle()
|
||||
}
|
||||
} else {
|
||||
timelineNavItem
|
||||
.opacity(isSideBarOpened ? 0 : 1)
|
||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||
}
|
||||
timelineNavItem
|
||||
.opacity(isSideBarOpened ? 0 : 1)
|
||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,14 +239,7 @@ struct ContentView: View {
|
||||
MainContent(damus: damus)
|
||||
.toolbar() {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
isSideBarOpened.toggle()
|
||||
} label: {
|
||||
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles, disable_animation: damus_state!.settings.disable_animation)
|
||||
.opacity(isSideBarOpened ? 0 : 1)
|
||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||
}
|
||||
.disabled(isSideBarOpened)
|
||||
TopbarSideMenuButton(damus_state: damus, isSideBarOpened: $isSideBarOpened)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
@@ -237,9 +260,11 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(DamusColors.adaptableWhite)
|
||||
.edgesIgnoringSafeArea(selected_timeline != .home ? [] : [.top, .bottom])
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
.overlay(
|
||||
SideMenuView(damus_state: damus_state!, isSidebarVisible: $isSideBarOpened.animation())
|
||||
SideMenuView(damus_state: damus_state!, isSidebarVisible: $isSideBarOpened.animation(), selected: $selected_timeline)
|
||||
)
|
||||
.navigationDestination(for: Route.self) { route in
|
||||
route.view(navigationCoordinator: navigationCoordinator, damusState: damus_state!)
|
||||
@@ -249,13 +274,28 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
|
||||
if !hide_bar {
|
||||
TabBar(nstatus: home.notification_status, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
|
||||
.padding([.bottom], 8)
|
||||
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
|
||||
} else {
|
||||
Text("")
|
||||
.damus_full_screen_cover($active_full_screen_item, damus_state: damus, content: { item in
|
||||
return item.view(damus_state: damus)
|
||||
})
|
||||
.overlay(alignment: .bottom) {
|
||||
if !hide_bar {
|
||||
if !isSideBarOpened {
|
||||
TabBar(nstatus: home.notification_status, navIsAtRoot: navIsAtRoot(), selected: $selected_timeline, headerOffset: $headerOffset, settings: damus.settings, action: switch_timeline)
|
||||
.padding([.bottom], 8)
|
||||
.background(selected_timeline != .home || (selected_timeline == .home && !self.navIsAtRoot()) ? DamusColors.adaptableWhite : DamusColors.adaptableWhite.opacity(abs(1.25 - (abs(headerOffset/100.0)))))
|
||||
.anchorPreference(key: HeaderBoundsKey.self, value: .bounds){$0}
|
||||
.overlayPreferenceValue(HeaderBoundsKey.self) { value in
|
||||
GeometryReader{ proxy in
|
||||
if let anchor = value{
|
||||
Color.clear
|
||||
.onAppear {
|
||||
tabHeight = proxy[anchor].height
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -413,6 +453,9 @@ struct ContentView: View {
|
||||
.onReceive(handle_notify(.present_sheet)) { sheet in
|
||||
self.active_sheet = sheet
|
||||
}
|
||||
.onReceive(handle_notify(.present_full_screen_item)) { item in
|
||||
self.active_full_screen_item = item
|
||||
}
|
||||
.onReceive(handle_notify(.zapping)) { zap_ev in
|
||||
guard !zap_ev.is_custom else {
|
||||
return
|
||||
@@ -678,7 +721,7 @@ struct ContentView: View {
|
||||
wallet: WalletModel(settings: settings),
|
||||
nav: self.navigationCoordinator,
|
||||
music: MusicController(onChange: music_changed),
|
||||
video: VideoController(),
|
||||
video: DamusVideoCoordinator(),
|
||||
ndb: ndb,
|
||||
quote_reposts: .init(our_pubkey: pubkey),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||
@@ -742,6 +785,25 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct TopbarSideMenuButton: View {
|
||||
let damus_state: DamusState
|
||||
@Binding var isSideBarOpened: Bool
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
isSideBarOpened.toggle()
|
||||
} label: {
|
||||
ProfilePicView(pubkey: damus_state.pubkey, size: 32, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||
.opacity(isSideBarOpened ? 0 : 1)
|
||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||
.accessibilityHidden(true) // Knowing there is a profile picture here leads to no actionable outcome to VoiceOver users, so it is best not to show it
|
||||
}
|
||||
.accessibilityIdentifier(AppAccessibilityIdentifiers.main_side_menu_button.rawValue)
|
||||
.accessibilityLabel(NSLocalizedString("Side menu", comment: "Accessibility label for the side menu button at the topbar"))
|
||||
.disabled(isSideBarOpened)
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView(keypair: Keypair(pubkey: test_pubkey, privkey: nil), appDelegate: nil)
|
||||
@@ -869,7 +931,6 @@ enum FindEventType {
|
||||
|
||||
enum FoundEvent {
|
||||
case profile(Pubkey)
|
||||
case invalid_profile(NostrEvent)
|
||||
case event(NostrEvent)
|
||||
}
|
||||
|
||||
@@ -926,10 +987,6 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
||||
switch query {
|
||||
case .profile:
|
||||
if ev.known_kind == .metadata {
|
||||
guard state.ndb.lookup_profile_key(ev.pubkey) != nil else {
|
||||
callback(.invalid_profile(ev))
|
||||
return
|
||||
}
|
||||
callback(.profile(ev.pubkey))
|
||||
}
|
||||
case .event:
|
||||
@@ -938,17 +995,16 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
||||
case .eose:
|
||||
if !has_event {
|
||||
attempts += 1
|
||||
if attempts == state.pool.our_descriptors.count / 2 {
|
||||
callback(nil)
|
||||
if attempts >= state.pool.our_descriptors.count {
|
||||
callback(nil) // If we could not find any events in any of the relays we are connected to, send back nil
|
||||
}
|
||||
state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||
}
|
||||
state.pool.unsubscribe(sub_id: subid, to: [relay_id]) // We are only finding an event once, so close subscription on eose
|
||||
case .notice:
|
||||
break
|
||||
case .auth:
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>river</string>
|
||||
<string>alby</string>
|
||||
<string>albygo</string>
|
||||
<string>bitcoinbeach</string>
|
||||
<string>breez</string>
|
||||
<string>muun</string>
|
||||
|
||||
@@ -34,13 +34,13 @@ class DamusState: HeadlessDamusState {
|
||||
let wallet: WalletModel
|
||||
let nav: NavigationCoordinator
|
||||
let music: MusicController?
|
||||
let video: VideoController
|
||||
let video: DamusVideoCoordinator
|
||||
let ndb: Ndb
|
||||
var purple: DamusPurple
|
||||
var push_notification_client: PushNotificationClient
|
||||
let emoji_provider: EmojiProvider
|
||||
|
||||
init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [RelayURL], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) {
|
||||
init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [RelayURL], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) {
|
||||
self.pool = pool
|
||||
self.keypair = keypair
|
||||
self.likes = likes
|
||||
@@ -141,7 +141,7 @@ class DamusState: HeadlessDamusState {
|
||||
wallet: WalletModel(settings: settings),
|
||||
nav: navigationCoordinator,
|
||||
music: MusicController(onChange: { _ in }),
|
||||
video: VideoController(),
|
||||
video: DamusVideoCoordinator(),
|
||||
ndb: ndb,
|
||||
quote_reposts: .init(our_pubkey: pubkey),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||
@@ -209,7 +209,7 @@ class DamusState: HeadlessDamusState {
|
||||
wallet: WalletModel(settings: UserSettingsStore()),
|
||||
nav: NavigationCoordinator(),
|
||||
music: nil,
|
||||
video: VideoController(),
|
||||
video: DamusVideoCoordinator(),
|
||||
ndb: .empty,
|
||||
quote_reposts: .init(our_pubkey: empty_pub),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||
|
||||
@@ -213,3 +213,27 @@ enum HighlightSource: Hashable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ShareContent {
|
||||
let title: String
|
||||
let content: ContentType
|
||||
|
||||
enum ContentType {
|
||||
case link(URL)
|
||||
case media([PreUploadedMedia])
|
||||
}
|
||||
|
||||
func getLinkURL() -> URL? {
|
||||
if case let .link(url) = content {
|
||||
return url
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getMediaArray() -> [PreUploadedMedia] {
|
||||
if case let .media(mediaArray) = content {
|
||||
return mediaArray
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,11 +77,19 @@ enum MediaUpload {
|
||||
}
|
||||
}
|
||||
|
||||
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
|
||||
protocol ImageUploadModelProtocol {
|
||||
init()
|
||||
|
||||
func start(media: MediaUpload, uploader: any MediaUploaderProtocol, mediaType: ImageUploadMediaType, keypair: Keypair?) async -> ImageUploadResult
|
||||
}
|
||||
|
||||
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject, ImageUploadModelProtocol {
|
||||
@Published var progress: Double? = nil
|
||||
|
||||
func start(media: MediaUpload, uploader: MediaUploader, keypair: Keypair? = nil) async -> ImageUploadResult {
|
||||
let res = await create_upload_request(mediaToUpload: media, mediaUploader: uploader, progress: self, keypair: keypair)
|
||||
override required init() { }
|
||||
|
||||
func start(media: MediaUpload, uploader: any MediaUploaderProtocol, mediaType: ImageUploadMediaType, keypair: Keypair? = nil) async -> ImageUploadResult {
|
||||
let res = await AttachMediaUtility.create_upload_request(mediaToUpload: media, mediaUploader: uploader, mediaType: mediaType, progress: self, keypair: keypair)
|
||||
|
||||
switch res {
|
||||
case .success(_):
|
||||
@@ -89,10 +97,17 @@ class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
|
||||
self.progress = nil
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||
}
|
||||
case .failed(_):
|
||||
case .failed(let error):
|
||||
DispatchQueue.main.async {
|
||||
self.progress = nil
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
if let nsError = error as NSError?,
|
||||
nsError.domain == NSURLErrorDomain,
|
||||
nsError.code == NSURLErrorCancelled {
|
||||
print("Upload forced cancelled by user after Cancelling the Post, no feedback triggered.")
|
||||
} else {
|
||||
// Trigger feedback for all other errors
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,18 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum MediaUploader: String, CaseIterable, Identifiable, StringCodable {
|
||||
protocol MediaUploaderProtocol: Identifiable {
|
||||
var nameParam: String { get }
|
||||
var mediaTypeParam: String { get }
|
||||
var supportsVideo: Bool { get }
|
||||
var requiresNip98: Bool { get }
|
||||
var postAPI: String { get }
|
||||
|
||||
func getMediaURL(from data: Data) -> String?
|
||||
func mediaTypeValue(for mediaType: ImageUploadMediaType) -> String?
|
||||
}
|
||||
|
||||
enum MediaUploader: String, CaseIterable, MediaUploaderProtocol, StringCodable {
|
||||
var id: String { self.rawValue }
|
||||
case nostrBuild
|
||||
case nostrcheck
|
||||
@@ -33,6 +44,19 @@ enum MediaUploader: String, CaseIterable, Identifiable, StringCodable {
|
||||
}
|
||||
}
|
||||
|
||||
var mediaTypeParam: String {
|
||||
return "media_type"
|
||||
}
|
||||
|
||||
func mediaTypeValue(for mediaType: ImageUploadMediaType) -> String? {
|
||||
switch mediaType {
|
||||
case .normal:
|
||||
return nil
|
||||
case .profile_picture:
|
||||
return "avatar"
|
||||
}
|
||||
}
|
||||
|
||||
var supportsVideo: Bool {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
@@ -42,6 +66,15 @@ enum MediaUploader: String, CaseIterable, Identifiable, StringCodable {
|
||||
}
|
||||
}
|
||||
|
||||
var requiresNip98: Bool {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
return true
|
||||
case .nostrcheck:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
struct Model: Identifiable, Hashable {
|
||||
var id: String { self.tag }
|
||||
var index: Int
|
||||
|
||||
@@ -39,7 +39,7 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
|
||||
switch self {
|
||||
case .none:
|
||||
let displayName: String
|
||||
if TranslationService.isAppleTranslationSupported {
|
||||
if TranslationService.isAppleTranslationPopoverSupported {
|
||||
displayName = NSLocalizedString("apple_translation_service", value: "Apple", comment: "Dropdown option for selecting Apple as a translation service.")
|
||||
} else {
|
||||
displayName = NSLocalizedString("none_translation_service", value: "None", comment: "Dropdown option for selecting no translation service.")
|
||||
@@ -58,7 +58,7 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
|
||||
}
|
||||
}
|
||||
|
||||
static var isAppleTranslationSupported: Bool {
|
||||
static var isAppleTranslationPopoverSupported: Bool {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
return false
|
||||
#else
|
||||
|
||||
@@ -180,17 +180,11 @@ class UserSettingsStore: ObservableObject {
|
||||
@Setting(key: "auto_translate", default_value: true)
|
||||
var auto_translate: Bool
|
||||
|
||||
@Setting(key: "translate_offline", default_value: true)
|
||||
var translate_offline: Bool
|
||||
|
||||
@Setting(key: "show_general_statuses", default_value: true)
|
||||
var show_general_statuses: Bool
|
||||
|
||||
@Setting(key: "show_music_statuses", default_value: true)
|
||||
var show_music_statuses: Bool
|
||||
|
||||
@Setting(key: "show_only_preferred_languages", default_value: false)
|
||||
var show_only_preferred_languages: Bool
|
||||
|
||||
@Setting(key: "multiple_events_per_pubkey", default_value: false)
|
||||
var multiple_events_per_pubkey: Bool
|
||||
|
||||
@@ -46,6 +46,7 @@ enum Wallet: String, CaseIterable, Identifiable, StringCodable {
|
||||
case bitcoinbeach
|
||||
case blixtwallet
|
||||
case river
|
||||
case albygo
|
||||
|
||||
var model: Model {
|
||||
switch self {
|
||||
@@ -90,6 +91,9 @@ enum Wallet: String, CaseIterable, Identifiable, StringCodable {
|
||||
case .river:
|
||||
return .init(index: 12, tag: "river", displayName: "River", link: "river://",
|
||||
appStoreLink: "https://apps.apple.com/us/app/river-buy-mine-bitcoin/id1536176542", image: "river")
|
||||
case .albygo:
|
||||
return .init(index: 13, tag: "albygo", displayName: "Alby Go", link: "alby:",
|
||||
appStoreLink: "https://apps.apple.com/us/app/alby-go/id6471335774", image: "alby-go")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,15 @@ struct SwipeToDismissModifier: ViewModifier {
|
||||
var onDismiss: () -> Void
|
||||
@State private var offset: CGSize = .zero
|
||||
@GestureState private var viewOffset: CGSize = .zero
|
||||
|
||||
let threshold_offset: CGFloat = 100.0
|
||||
let minimum_opacity: CGFloat = 0.1
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.offset(y: viewOffset.height)
|
||||
.animation(.interactiveSpring(), value: viewOffset)
|
||||
.opacity(max(min(1.0 - (abs(offset.height) / threshold_offset), 1.0), minimum_opacity))
|
||||
.simultaneousGesture(
|
||||
DragGesture(minimumDistance: minDistance ?? 10)
|
||||
.updating($viewOffset, body: { value, gestureState, transaction in
|
||||
@@ -28,7 +32,7 @@ struct SwipeToDismissModifier: ViewModifier {
|
||||
}
|
||||
}
|
||||
.onEnded { _ in
|
||||
if abs(offset.height) > 100 {
|
||||
if abs(offset.height) > threshold_offset {
|
||||
onDismiss()
|
||||
} else {
|
||||
offset = .zero
|
||||
|
||||
42
damus/Notify/PresentFullScreenItemNotify.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// PresentFullScreenItemNotify.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2024-11-01.
|
||||
//
|
||||
|
||||
struct PresentFullScreenItemNotify: Notify {
|
||||
typealias Payload = FullScreenItem
|
||||
var payload: Payload
|
||||
}
|
||||
|
||||
extension NotifyHandler {
|
||||
static var present_full_screen_item: NotifyHandler<PresentFullScreenItemNotify> {
|
||||
.init()
|
||||
}
|
||||
}
|
||||
|
||||
extension Notifications {
|
||||
static func present_full_screen_item(_ item: FullScreenItem) -> Notifications<PresentFullScreenItemNotify> {
|
||||
.init(.init(payload: item))
|
||||
}
|
||||
}
|
||||
|
||||
/// Tell the app to present an item in full screen. Use this when presenting items coming from a timeline or any lazy stack.
|
||||
///
|
||||
/// ## Usage notes
|
||||
///
|
||||
/// Use this instead of `.damus_full_screen_cover` when the source view is on a lazy stack or timeline.
|
||||
///
|
||||
/// The reason is that when using a full screen modifier in those scenarios, the full screen view may abruptly disappear.
|
||||
/// One example is when showing videos from the timeline in full screen, where changing the orientation of the device (landscape/portrait)
|
||||
/// can cause the source view to be unloaded by the lazy stack, making your full screen overlay to simply disappear, causing a feeling of flakiness to the app
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// The requests from this function will be received and handled at the top level app view (`ContentView`), which contains a `.damus_full_screen_cover`.
|
||||
///
|
||||
func present(full_screen_item: FullScreenItem) {
|
||||
notify(.present_full_screen_item(full_screen_item))
|
||||
}
|
||||
|
||||
@@ -31,4 +31,7 @@ class Constants {
|
||||
static let DAMUS_WEBSITE_LOCAL_TEST_URL: URL = URL(string: "http://localhost:3000")!
|
||||
static let DAMUS_WEBSITE_STAGING_URL: URL = URL(string: "https://staging.damus.io")!
|
||||
static let DAMUS_WEBSITE_PRODUCTION_URL: URL = URL(string: "https://damus.io")!
|
||||
|
||||
// MARK: General constants
|
||||
static let GIF_IMAGE_TYPE: String = "com.compuserve.gif"
|
||||
}
|
||||
|
||||
@@ -259,11 +259,10 @@ func should_translate(event: NostrEvent, our_keypair: Keypair, note_lang: String
|
||||
}
|
||||
|
||||
if let note_lang {
|
||||
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||
let currentLanguage = localeToLanguage(Locale.current.identifier)
|
||||
|
||||
// Don't translate if its in our preferred languages
|
||||
guard !preferredLanguages.contains(note_lang) else {
|
||||
// if its the same, give up and don't retry
|
||||
// Don't translate if the note is in our current language
|
||||
guard currentLanguage != note_lang else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,22 @@ extension KFOptionSetter {
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/// This allows you to observe the size of the image, and get a callback when the size changes
|
||||
/// This is useful for when you need to layout views based on the size of the image
|
||||
/// - Parameter size_changed: A callback that will be called when the size of the image changes
|
||||
/// - Returns: The same KFOptionSetter instance
|
||||
func observe_image_size(size_changed: @escaping (CGSize) -> Void) -> Self {
|
||||
let modifier = AnyImageModifier { image -> KFCrossPlatformImage in
|
||||
let image_size = image.size
|
||||
DispatchQueue.main.async { [size_changed, image_size] in
|
||||
size_changed(image_size)
|
||||
}
|
||||
return image
|
||||
}
|
||||
options.imageModifier = modifier
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
let MAX_FILE_SIZE = 20_971_520 // 20MiB
|
||||
|
||||