Compare commits
37 Commits
change-emo
...
profile-vi
| Author | SHA1 | Date | |
|---|---|---|---|
|
1767a677bb
|
|||
|
|
dba1799df0 | ||
|
|
2db3d7310f | ||
|
|
10b1cf64ae | ||
|
|
afdd3f1d43 | ||
|
|
1b8e3fe184 | ||
|
|
8ab1c6a899 | ||
|
|
e8fae19b97 | ||
|
|
63e70605fc | ||
|
|
35df9f7ab7 | ||
|
|
605d88add1 | ||
|
|
2b0a7d126d | ||
|
|
6e2c133faa | ||
|
|
9885ff1912 | ||
|
|
abb818bbd4 | ||
|
|
f1dc023e18 | ||
|
|
4a332c7ffa | ||
|
|
616f730ae5 | ||
|
|
164cea96f3 | ||
|
|
fa70c376b1 | ||
|
847f31f5a6
|
|||
|
|
fd130b78e7 | ||
|
|
0be0273121 | ||
|
|
b349de22b7 | ||
|
|
cc2d196705 | ||
|
|
53be29efc2 | ||
|
|
529ee63f29 | ||
|
|
490e8ec1fb | ||
|
|
df267ffd04 | ||
|
|
b771e8f49a | ||
|
|
a88e80a346 | ||
|
8ac9863765
|
|||
|
|
4a851501a1 | ||
|
|
4ccfe81558 | ||
|
|
e7ed9dfe86 | ||
|
|
0dce7aea45 | ||
|
|
6376c61bad |
76
CHANGELOG.md
76
CHANGELOG.md
@@ -1,3 +1,79 @@
|
||||
## [1.9 (14)] - 2024-07-14
|
||||
|
||||
### Added
|
||||
|
||||
- Completely new threads experience that is easier and more pleasant to use (Daniel D’Aquino)
|
||||
- Add emoji search to emoji picker (Terry Yiu)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Added first aid contact damus support email (alltheseas)
|
||||
- Disable mutiny wallet button (William Casarin)
|
||||
- Make friends show up first when searching for profiles (Terry Yiu)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix crash on profile page when there are profile updates (William Casarin)
|
||||
- Fix crash when adding duplicate mute items (William Casarin)
|
||||
- Fix pretty bad crash when building flatbuffer profiles (William Casarin)
|
||||
- Fix reactions view to not show reactions from replies on parent note (Terry Yiu)
|
||||
- Fix missing Mute button in profile view menu (Terry Yiu)
|
||||
- Fixed wallet not disconnecting when a user logs out (ericholguin)
|
||||
- Fix stale feed issue when follow list is too big (Daniel D’Aquino)
|
||||
|
||||
[1.9 (14)]: https://github.com/damus-io/damus/releases/tag/v1.9-14
|
||||
|
||||
## [1.8] - 2024-05-11
|
||||
|
||||
### Added
|
||||
|
||||
- Added nip10 marker replies (William Casarin)
|
||||
- Add marker nip10 support when reading notes (William Casarin)
|
||||
- Added title image and tags to longform events (ericholguin)
|
||||
- Add First Aid solution for users who do not have a contact list created for their account (Daniel D’Aquino)
|
||||
- Relay fees metadata (ericholguin)
|
||||
- Added callbackuri for a better ux when connecting mutiny wallet nwc (ericholguin)
|
||||
- Add event content preview to the full screen carousel (Daniel D’Aquino)
|
||||
- Show list of quoted reposts in threads (William Casarin)
|
||||
- Proxy Tags are now viewable on Selected Events (ericholguin)
|
||||
- Connect to Mutiny Wallet Button (ericholguin)
|
||||
- Add ability to mute words, add new mutelist interface (Charlie) (William Casarin)
|
||||
- Add ability to mute hashtag from SearchView (Charlie Fish)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Change reactions to use a native looking emoji picker (Terry Yiu)
|
||||
- Relay detail design (ericholguin)
|
||||
- Updated Zeus logo (ericholguin)
|
||||
- Improve UX around video playback (Daniel D’Aquino)
|
||||
- Moved paste nwc button to main wallet view (ericholguin)
|
||||
- Errors with an NWC will show as an alert (ericholguin)
|
||||
- Relay config view user interface (ericholguin)
|
||||
- Always strip GPS data from images (kernelkind)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix thread bug where a quote isn't picked up as a reply (William Casarin)
|
||||
- Fixed threads not loading sometimes (William Casarin)
|
||||
- Fixed issue where some replies were including the q tag (William Casarin)
|
||||
- Fixed issue where timeline was scrolling when it isn't supposed to (William Casarin)
|
||||
- Fix issue where bootstrap relays would inadvertently be added to the user's list on connectivity issues (Daniel D’Aquino)
|
||||
- Fix broken GIF uploads (Daniel D’Aquino)
|
||||
- Fix ghost notifications caused by Purple impending expiration notifications (Daniel D’Aquino)
|
||||
- Improve reliability of contact list creation during onboarding (Daniel D’Aquino)
|
||||
- Fix emoji reactions being cut off (ericholguin)
|
||||
- Fix image indicators to limit number of dots to not spill screen beyond visible margins (ericholguin)
|
||||
- Fix bug that would cause connection issues with relays defined with a trailing slash URL, and an inability to delete them. (Daniel D’Aquino)
|
||||
- Issue where NWC Scanner view would not dismiss after a failed scan/paste (ericholguin)
|
||||
|
||||
|
||||
|
||||
[1.8]: https://github.com/damus-io/damus/releases/tag/v1.8
|
||||
|
||||
## [1.7-rc2] - 2024-02-28
|
||||
|
||||
### Added
|
||||
|
||||
@@ -399,6 +399,7 @@
|
||||
5C14C29D2BBBA40B00079FD2 /* RelayAdminDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */; };
|
||||
5C14C29F2BBBA5C600079FD2 /* RelayNipList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29E2BBBA5C600079FD2 /* RelayNipList.swift */; };
|
||||
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; };
|
||||
5C4D9EA72C042FA5005EA0F7 /* HighlightPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4D9EA62C042FA5005EA0F7 /* HighlightPostView.swift */; };
|
||||
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
|
||||
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
|
||||
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */; };
|
||||
@@ -406,6 +407,12 @@
|
||||
5C7389B12B6EFA7100781E0A /* ProxyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B02B6EFA7100781E0A /* ProxyView.swift */; };
|
||||
5C7389B72B9E692E00781E0A /* MutinyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B62B9E692E00781E0A /* MutinyButton.swift */; };
|
||||
5C7389B92B9E69ED00781E0A /* MutinyGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */; };
|
||||
5C8711DE2C460C06007879C2 /* PostingTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8711DD2C460C06007879C2 /* PostingTimelineView.swift */; };
|
||||
5CC8529D2BD741CD0039FFC5 /* HighlightEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */; };
|
||||
5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC8529E2BD744F60039FFC5 /* HighlightView.swift */; };
|
||||
5CC852A22BDED9B90039FFC5 /* HighlightDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */; };
|
||||
5CC852A42BDF3CA10039FFC5 /* HighlightLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */; };
|
||||
5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */; };
|
||||
5CC868DD2AA29B3200FB22BA /* NeutralButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */; };
|
||||
5CF2DCCC2AA3AF0B00984B8D /* RelayPicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2DCCB2AA3AF0B00984B8D /* RelayPicView.swift */; };
|
||||
5CF2DCCE2AABE1A500984B8D /* DamusLightGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */; };
|
||||
@@ -1334,6 +1341,7 @@
|
||||
5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayAdminDetail.swift; sourceTree = "<group>"; };
|
||||
5C14C29E2BBBA5C600079FD2 /* RelayNipList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayNipList.swift; sourceTree = "<group>"; };
|
||||
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; };
|
||||
5C4D9EA62C042FA5005EA0F7 /* HighlightPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightPostView.swift; sourceTree = "<group>"; };
|
||||
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
|
||||
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
|
||||
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButtonStyle.swift; sourceTree = "<group>"; };
|
||||
@@ -1341,6 +1349,12 @@
|
||||
5C7389B02B6EFA7100781E0A /* ProxyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyView.swift; sourceTree = "<group>"; };
|
||||
5C7389B62B9E692E00781E0A /* MutinyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyButton.swift; sourceTree = "<group>"; };
|
||||
5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyGradient.swift; sourceTree = "<group>"; };
|
||||
5C8711DD2C460C06007879C2 /* PostingTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingTimelineView.swift; sourceTree = "<group>"; };
|
||||
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightEvent.swift; sourceTree = "<group>"; };
|
||||
5CC8529E2BD744F60039FFC5 /* HighlightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightView.swift; sourceTree = "<group>"; };
|
||||
5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDescription.swift; sourceTree = "<group>"; };
|
||||
5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightLink.swift; sourceTree = "<group>"; };
|
||||
5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightEventRef.swift; sourceTree = "<group>"; };
|
||||
5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeutralButtonStyle.swift; sourceTree = "<group>"; };
|
||||
5CF2DCCB2AA3AF0B00984B8D /* RelayPicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPicView.swift; sourceTree = "<group>"; };
|
||||
5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLightGradient.swift; sourceTree = "<group>"; };
|
||||
@@ -1668,6 +1682,7 @@
|
||||
B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */,
|
||||
B533694D2B66D791008A805E /* MutelistManager.swift */,
|
||||
D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */,
|
||||
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
@@ -2402,6 +2417,7 @@
|
||||
4CC7AAEE297F11B300430951 /* Events */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CC852A02BDED9970039FFC5 /* Highlight */,
|
||||
4CA927682A290F8F0098A105 /* Components */,
|
||||
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */,
|
||||
4CC7AAF5297F1A6A00430951 /* EventBody.swift */,
|
||||
@@ -2433,6 +2449,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */,
|
||||
5C8711DD2C460C06007879C2 /* PostingTimelineView.swift */,
|
||||
);
|
||||
path = Timeline;
|
||||
sourceTree = "<group>";
|
||||
@@ -2699,6 +2716,18 @@
|
||||
path = Images;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5CC852A02BDED9970039FFC5 /* Highlight */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CC8529E2BD744F60039FFC5 /* HighlightView.swift */,
|
||||
5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */,
|
||||
5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */,
|
||||
5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */,
|
||||
5C4D9EA62C042FA5005EA0F7 /* HighlightPostView.swift */,
|
||||
);
|
||||
path = Highlight;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7C0F392D29B57C8F0039859C /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -3149,10 +3178,12 @@
|
||||
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
|
||||
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
|
||||
4CC14FF12A73FCDB007AEB17 /* Pubkey.swift in Sources */,
|
||||
5C8711DE2C460C06007879C2 /* PostingTimelineView.swift in Sources */,
|
||||
4CA9275D2A28FF630098A105 /* LongformView.swift in Sources */,
|
||||
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
|
||||
D7EDED1E2B11797D0018B19C /* LongformEvent.swift in Sources */,
|
||||
504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */,
|
||||
5C4D9EA72C042FA5005EA0F7 /* HighlightPostView.swift in Sources */,
|
||||
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */,
|
||||
D7FD12262BD345A700CF195B /* FirstAidSettingsView.swift in Sources */,
|
||||
D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */,
|
||||
@@ -3183,6 +3214,7 @@
|
||||
4C32B94D2A9AD44700DC3548 /* Offset.swift in Sources */,
|
||||
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
|
||||
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */,
|
||||
5CC852A42BDF3CA10039FFC5 /* HighlightLink.swift in Sources */,
|
||||
4C32B9552A9AD44700DC3548 /* ByteBuffer.swift in Sources */,
|
||||
4C32B95B2A9AD44700DC3548 /* NativeObject.swift in Sources */,
|
||||
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */,
|
||||
@@ -3322,6 +3354,7 @@
|
||||
4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */,
|
||||
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */,
|
||||
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */,
|
||||
5CC852A22BDED9B90039FFC5 /* HighlightDescription.swift in Sources */,
|
||||
4C94D6432BA5AEFE00C26EFF /* QuoteRepostsView.swift in Sources */,
|
||||
D7EDED332B12ACAE0018B19C /* DamusUserDefaults.swift in Sources */,
|
||||
4CA352AE2A76C1AC003BB08B /* FollowedNotify.swift in Sources */,
|
||||
@@ -3365,7 +3398,9 @@
|
||||
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
|
||||
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */,
|
||||
4CA352A22A76AEC5003BB08B /* LikedNotify.swift in Sources */,
|
||||
5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */,
|
||||
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */,
|
||||
5CC8529D2BD741CD0039FFC5 /* HighlightEvent.swift in Sources */,
|
||||
4C9146FD2A2A87C200DDEA40 /* wasm.c in Sources */,
|
||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
||||
@@ -3477,6 +3512,7 @@
|
||||
B51C1CEB2B55A60A00E312A9 /* MuteDurationMenu.swift in Sources */,
|
||||
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
|
||||
4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */,
|
||||
5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */,
|
||||
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
|
||||
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
|
||||
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
|
||||
@@ -4037,6 +4073,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -4063,6 +4100,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.11;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
@@ -4086,6 +4124,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -4112,6 +4151,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.11;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xF2",
|
||||
"green" : "0xD8",
|
||||
"red" : "0xF4"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x45",
|
||||
"green" : "0x17",
|
||||
"red" : "0x47"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -12,31 +12,25 @@ let RECTANGLE_GRADIENT = LinearGradient(gradient: Gradient(colors: [
|
||||
DamusColors.blue
|
||||
]), startPoint: .leading, endPoint: .trailing)
|
||||
|
||||
struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
|
||||
struct CustomPicker<SelectionValue: Hashable>: View {
|
||||
|
||||
let tabs: [(String, SelectionValue)]
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
|
||||
@Namespace var picker
|
||||
@Binding var selection: SelectionValue
|
||||
@ViewBuilder let content: Content
|
||||
|
||||
|
||||
public var body: some View {
|
||||
let contentMirror = Mirror(reflecting: content)
|
||||
let blocksCount = Mirror(reflecting: contentMirror.descendant("value")!).children.count
|
||||
HStack {
|
||||
ForEach(0..<blocksCount, id: \.self) { index in
|
||||
let tupleBlock = contentMirror.descendant("value", ".\(index)")
|
||||
let text = Mirror(reflecting: tupleBlock!).descendant("content") as! Text
|
||||
let tag = Mirror(reflecting: tupleBlock!).descendant("modifier", "value", "tagged") as! SelectionValue
|
||||
|
||||
ForEach(tabs, id: \.1) { (text, tag) in
|
||||
Button {
|
||||
withAnimation(.spring()) {
|
||||
selection = tag
|
||||
}
|
||||
} label: {
|
||||
text
|
||||
.padding(EdgeInsets(top: 15, leading: 0, bottom: 10, trailing: 0))
|
||||
Text(text).padding(EdgeInsets(top: 15, leading: 0, bottom: 10, trailing: 0))
|
||||
.font(.system(size: 14, weight: .heavy))
|
||||
.tag(tag)
|
||||
}
|
||||
.background(
|
||||
Group {
|
||||
|
||||
@@ -28,6 +28,7 @@ class DamusColors {
|
||||
static let green = Color("DamusGreen")
|
||||
static let purple = Color("DamusPurple")
|
||||
static let deepPurple = Color("DamusDeepPurple")
|
||||
static let highlight = Color("DamusHighlight")
|
||||
static let blue = Color("DamusBlue")
|
||||
static let bitcoin = Color("Bitcoin")
|
||||
static let success = Color("DamusSuccessPrimary")
|
||||
|
||||
@@ -9,16 +9,20 @@ import UIKit
|
||||
import SwiftUI
|
||||
|
||||
struct SelectableText: View {
|
||||
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent?
|
||||
let attributedString: AttributedString
|
||||
let textAlignment: NSTextAlignment
|
||||
|
||||
@State private var showHighlightPost = false
|
||||
@State private var selectedText = ""
|
||||
@State private var selectedTextHeight: CGFloat = .zero
|
||||
@State private var selectedTextWidth: CGFloat = .zero
|
||||
|
||||
|
||||
let size: EventViewKind
|
||||
|
||||
init(attributedString: AttributedString, textAlignment: NSTextAlignment? = nil, size: EventViewKind) {
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent?, attributedString: AttributedString, textAlignment: NSTextAlignment? = nil, size: EventViewKind) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.attributedString = attributedString
|
||||
self.textAlignment = textAlignment ?? NSTextAlignment.natural
|
||||
self.size = size
|
||||
@@ -32,6 +36,9 @@ struct SelectableText: View {
|
||||
font: eventviewsize_to_uifont(size),
|
||||
fixedWidth: selectedTextWidth,
|
||||
textAlignment: self.textAlignment,
|
||||
enableHighlighting: self.enableHighlighting(),
|
||||
showHighlightPost: $showHighlightPost,
|
||||
selectedText: $selectedText,
|
||||
height: $selectedTextHeight
|
||||
)
|
||||
.padding([.leading, .trailing], -1.0)
|
||||
@@ -46,8 +53,48 @@ struct SelectableText: View {
|
||||
self.selectedTextWidth = newSize.width
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showHighlightPost) {
|
||||
if let event {
|
||||
HighlightPostView(damus_state: damus_state, event: event, selectedText: $selectedText)
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationDetents([.height(selectedTextHeight + 150), .medium, .large])
|
||||
}
|
||||
}
|
||||
.frame(height: selectedTextHeight)
|
||||
}
|
||||
|
||||
func enableHighlighting() -> Bool {
|
||||
self.event != nil
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class TextView: UITextView {
|
||||
@Binding var showHighlightPost: Bool
|
||||
@Binding var selectedText: String
|
||||
|
||||
init(frame: CGRect, textContainer: NSTextContainer?, showHighlightPost: Binding<Bool>, selectedText: Binding<String>) {
|
||||
self._showHighlightPost = showHighlightPost
|
||||
self._selectedText = selectedText
|
||||
super.init(frame: frame, textContainer: textContainer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
if action == #selector(highlightText(_:)) {
|
||||
return true
|
||||
}
|
||||
return super.canPerformAction(action, withSender: sender)
|
||||
}
|
||||
|
||||
@objc public func highlightText(_ sender: Any?) {
|
||||
guard let selectedRange = self.selectedTextRange else { return }
|
||||
selectedText = self.text(in: selectedRange) ?? ""
|
||||
showHighlightPost.toggle()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
||||
@@ -57,11 +104,13 @@ struct SelectableText: View {
|
||||
let font: UIFont
|
||||
let fixedWidth: CGFloat
|
||||
let textAlignment: NSTextAlignment
|
||||
|
||||
let enableHighlighting: Bool
|
||||
@Binding var showHighlightPost: Bool
|
||||
@Binding var selectedText: String
|
||||
@Binding var height: CGFloat
|
||||
|
||||
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
|
||||
let view = UITextView()
|
||||
func makeUIView(context: UIViewRepresentableContext<Self>) -> TextView {
|
||||
let view = TextView(frame: .zero, textContainer: nil, showHighlightPost: $showHighlightPost, selectedText: $selectedText)
|
||||
view.isEditable = false
|
||||
view.dataDetectorTypes = .all
|
||||
view.isSelectable = true
|
||||
@@ -71,10 +120,15 @@ struct SelectableText: View {
|
||||
view.textContainerInset.left = 1.0
|
||||
view.textContainerInset.right = 1.0
|
||||
view.textAlignment = textAlignment
|
||||
|
||||
let menuController = UIMenuController.shared
|
||||
let highlightItem = UIMenuItem(title: "Highlight", action: #selector(view.highlightText(_:)))
|
||||
menuController.menuItems = self.enableHighlighting ? [highlightItem] : []
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<Self>) {
|
||||
func updateUIView(_ uiView: TextView, context: UIViewRepresentableContext<Self>) {
|
||||
let mutableAttributedString = createNSAttributedString()
|
||||
uiView.attributedText = mutableAttributedString
|
||||
uiView.textAlignment = self.textAlignment
|
||||
|
||||
@@ -51,9 +51,9 @@ struct TranslateView: View {
|
||||
.foregroundColor(.gray)
|
||||
.font(.footnote)
|
||||
.padding([.top, .bottom], 10)
|
||||
|
||||
|
||||
if self.size == .selected {
|
||||
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
|
||||
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))
|
||||
|
||||
@@ -79,71 +79,15 @@ struct ContentView: View {
|
||||
@State var hide_bar: Bool = false
|
||||
@State var user_muted_confirm: Bool = false
|
||||
@State var confirm_overwrite_mutelist: Bool = false
|
||||
@SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies
|
||||
@State private var isSideBarOpened = false
|
||||
var home: HomeModel = HomeModel()
|
||||
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
||||
@AppStorage("has_seen_suggested_users") private var hasSeenOnboardingSuggestions = false
|
||||
let sub_id = UUID().description
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
// connect retry timer
|
||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
|
||||
var mystery: some View {
|
||||
Text("Are you lost?", comment: "Text asking the user if they are lost in the app.")
|
||||
.id("what")
|
||||
}
|
||||
|
||||
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
|
||||
var filters = ContentFilters.defaults(damus_state: damus_state!)
|
||||
filters.append(fstate.filter)
|
||||
return ContentFilters(filters: filters).filter
|
||||
}
|
||||
|
||||
var PostingTimelineView: some View {
|
||||
VStack {
|
||||
ZStack {
|
||||
TabView(selection: $filter_state) {
|
||||
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
|
||||
mystery
|
||||
|
||||
contentTimelineView(filter: content_filter(.posts))
|
||||
.tag(FilterState.posts)
|
||||
.id(FilterState.posts)
|
||||
contentTimelineView(filter: content_filter(.posts_and_replies))
|
||||
.tag(FilterState.posts_and_replies)
|
||||
.id(FilterState.posts_and_replies)
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
|
||||
if privkey != nil {
|
||||
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
|
||||
self.active_sheet = .post(.posting(.none))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .top, spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
CustomPicker(selection: $filter_state, content: {
|
||||
Text("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies).").tag(FilterState.posts)
|
||||
Text("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes).").tag(FilterState.posts_and_replies)
|
||||
})
|
||||
Divider()
|
||||
.frame(height: 1)
|
||||
}
|
||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||
}
|
||||
}
|
||||
|
||||
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
|
||||
TimelineView(events: home.events, loading: .constant(false), damus: damus_state, show_friend_icon: false, filter: filter) {
|
||||
PullDownSearchView(state: damus_state, on_cancel: {})
|
||||
}
|
||||
}
|
||||
|
||||
func navIsAtRoot() -> Bool {
|
||||
return navigationCoordinator.isAtRoot()
|
||||
}
|
||||
@@ -171,7 +115,7 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
case .home:
|
||||
PostingTimelineView
|
||||
PostingTimelineView(damus_state: damus_state!, home: home, active_sheet: $active_sheet)
|
||||
|
||||
case .notifications:
|
||||
NotificationsView(state: damus, notifications: home.notifications)
|
||||
|
||||
@@ -16,7 +16,7 @@ enum FilterState : Int {
|
||||
func filter(ev: NostrEvent) -> Bool {
|
||||
switch self {
|
||||
case .posts:
|
||||
return ev.known_kind == .boost || !ev.is_reply()
|
||||
return ev.known_kind == .boost || ev.known_kind == .highlight || !ev.is_reply()
|
||||
case .posts_and_replies:
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -9,31 +9,31 @@ import Foundation
|
||||
|
||||
|
||||
class CreateAccountModel: ObservableObject {
|
||||
@Published var real_name: String = ""
|
||||
@Published var nick_name: String = ""
|
||||
@Published var display_name: String = ""
|
||||
@Published var name: String = ""
|
||||
@Published var about: String = ""
|
||||
@Published var pubkey: Pubkey = .empty
|
||||
@Published var privkey: Privkey = .empty
|
||||
@Published var profile_image: URL? = nil
|
||||
|
||||
var rendered_name: String {
|
||||
if real_name.isEmpty {
|
||||
return nick_name
|
||||
if display_name.isEmpty {
|
||||
return name
|
||||
}
|
||||
return real_name
|
||||
return display_name
|
||||
}
|
||||
|
||||
var keypair: Keypair {
|
||||
return Keypair(pubkey: self.pubkey, privkey: self.privkey)
|
||||
}
|
||||
|
||||
init(real: String = "", nick: String = "", about: String = "") {
|
||||
init(display_name: String = "", name: String = "", about: String = "") {
|
||||
let keypair = generate_new_keypair()
|
||||
self.pubkey = keypair.pubkey
|
||||
self.privkey = keypair.privkey
|
||||
|
||||
self.real_name = real
|
||||
self.nick_name = nick
|
||||
self.display_name = display_name
|
||||
self.name = name
|
||||
self.about = about
|
||||
}
|
||||
}
|
||||
|
||||
34
damus/Models/HighlightEvent.swift
Normal file
34
damus/Models/HighlightEvent.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// HighlightEvent.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 4/22/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct HighlightEvent {
|
||||
let event: NostrEvent
|
||||
|
||||
var event_ref: String? = nil
|
||||
var url_ref: URL? = nil
|
||||
var context: String? = nil
|
||||
|
||||
static func parse(from ev: NostrEvent) -> HighlightEvent {
|
||||
var highlight = HighlightEvent(event: ev)
|
||||
|
||||
for tag in ev.tags {
|
||||
guard tag.count >= 2 else { continue }
|
||||
switch tag[0].string() {
|
||||
case "e": highlight.event_ref = tag[1].string()
|
||||
case "a": highlight.event_ref = tag[1].string()
|
||||
case "r": highlight.url_ref = URL(string: tag[1].string())
|
||||
case "context": highlight.context = tag[1].string()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return highlight
|
||||
}
|
||||
}
|
||||
@@ -184,7 +184,7 @@ class HomeModel: ContactsDelegate {
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case .chat, .longform, .text:
|
||||
case .chat, .longform, .text, .highlight:
|
||||
handle_text_event(sub_id: sub_id, ev)
|
||||
case .contacts:
|
||||
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
|
||||
@@ -586,7 +586,7 @@ class HomeModel: ContactsDelegate {
|
||||
func subscribe_to_home_filters(friends fs: [Pubkey]? = nil, relay_id: RelayURL? = nil) {
|
||||
// TODO: separate likes?
|
||||
var home_filter_kinds: [NostrKind] = [
|
||||
.text, .longform, .boost
|
||||
.text, .longform, .boost, .highlight
|
||||
]
|
||||
if !damus_state.settings.onlyzaps_mode {
|
||||
home_filter_kinds.append(.like)
|
||||
|
||||
@@ -111,12 +111,16 @@ class MutelistManager {
|
||||
private func add_mute_item(_ item: MuteItem) {
|
||||
switch item {
|
||||
case .user(_, _):
|
||||
guard !users.contains(item) else { return }
|
||||
users.insert(item)
|
||||
case .hashtag(_, _):
|
||||
guard !hashtags.contains(item) else { return }
|
||||
hashtags.insert(item)
|
||||
case .word(_, _):
|
||||
guard !words.contains(item) else { return }
|
||||
words.insert(item)
|
||||
case .thread(_, _):
|
||||
guard !threads.contains(item) else { return }
|
||||
threads.insert(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,11 +60,11 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
damus.pool.unsubscribe(sub_id: sub_id)
|
||||
damus.pool.unsubscribe(sub_id: prof_subid)
|
||||
}
|
||||
|
||||
|
||||
func subscribe() {
|
||||
var text_filter = NostrFilter(kinds: [.text, .longform])
|
||||
var text_filter = NostrFilter(kinds: [.text, .longform, .highlight])
|
||||
var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost])
|
||||
|
||||
|
||||
profile_filter.authors = [pubkey]
|
||||
|
||||
text_filter.authors = [pubkey]
|
||||
|
||||
@@ -36,7 +36,7 @@ class SearchModel: ObservableObject {
|
||||
func subscribe() {
|
||||
// since 1 month
|
||||
search.limit = self.limit
|
||||
search.kinds = [.text, .like, .longform]
|
||||
search.kinds = [.text, .like, .longform, .highlight]
|
||||
|
||||
//likes_filter.ids = ref_events.referenced_ids!
|
||||
|
||||
|
||||
@@ -11,13 +11,15 @@ import Foundation
|
||||
class ThreadModel: ObservableObject {
|
||||
@Published var event: NostrEvent
|
||||
let original_event: NostrEvent
|
||||
let highlight: String?
|
||||
var event_map: Set<NostrEvent>
|
||||
|
||||
init(event: NostrEvent, damus_state: DamusState) {
|
||||
init(event: NostrEvent, damus_state: DamusState, highlight: String? = nil) {
|
||||
self.damus_state = damus_state
|
||||
self.event_map = Set()
|
||||
self.event = event
|
||||
self.original_event = event
|
||||
self.highlight = highlight
|
||||
add_event(event, keypair: damus_state.keypair)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ enum NostrKind: UInt32, Codable {
|
||||
case longform = 30023
|
||||
case zap = 9735
|
||||
case zap_request = 9734
|
||||
case highlight = 9802
|
||||
case nwc_request = 23194
|
||||
case nwc_response = 23195
|
||||
case http_auth = 27235
|
||||
|
||||
@@ -36,6 +36,13 @@ let test_short_note =
|
||||
createdAt: UInt32(Date().timeIntervalSince1970 - 100)
|
||||
)!
|
||||
|
||||
let test_super_short_note =
|
||||
NostrEvent(
|
||||
content: "A",
|
||||
keypair: jack_keypair,
|
||||
createdAt: UInt32(Date().timeIntervalSince1970 - 100)
|
||||
)!
|
||||
|
||||
let test_note_json_with_escaped_slash = "{\"tags\":[],\"pubkey\":\"f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9\",\"content\":\"https:\\/\\/cdn.nostr.build\\/i\\/5c1d3296f66c2630131bf123106486aeaf051ed8466031c0e0532d70b33cddb2.jpg\",\"created_at\":1691864981,\"kind\":1,\"sig\":\"fc0033aa3d4df50b692a5b346fa816fdded698de2045e36e0642a021391468c44ca69c2471adc7e92088131872d4aaa1e90ea6e1ad97f3cc748f4aed96dfae18\",\"id\":\"e8f6eca3b161abba034dac9a02bb6930ecde9fd2fb5d6c5f22a05526e11382cb\"}"
|
||||
let test_encoded_note_with_image = NostrEvent.owned_from_json(json: test_note_json_with_escaped_slash)
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ import Foundation
|
||||
class Constants {
|
||||
//static let EXAMPLE_DEMOS: DamusState = .empty
|
||||
static let DAMUS_APP_GROUP_IDENTIFIER: String = "group.com.damus"
|
||||
static let DEVICE_TOKEN_RECEIVER_PRODUCTION_URL: URL = URL(string: "https://notify.damus.io:8000/user-info")!
|
||||
static let DEVICE_TOKEN_RECEIVER_PRODUCTION_URL: URL = URL(string: "http://45.33.32.5:8000/user-info")!
|
||||
static let DEVICE_TOKEN_RECEIVER_TEST_URL: URL = URL(string: "http://localhost:8000/user-info")!
|
||||
static let DEVICE_TOKEN_REVOKER_PRODUCTION_URL: URL = URL(string: "https://notify.damus.io:8000/user-info/remove")!
|
||||
static let DEVICE_TOKEN_REVOKER_PRODUCTION_URL: URL = URL(string: "http://45.33.32.5:8000/user-info/remove")!
|
||||
static let DEVICE_TOKEN_REVOKER_TEST_URL: URL = URL(string: "http://localhost:8000/user-info/remove")!
|
||||
static let MAIN_APP_BUNDLE_IDENTIFIER: String = "com.jb55.damus2"
|
||||
static let NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER: String = "com.jb55.damus2.DamusNotificationService"
|
||||
|
||||
@@ -31,7 +31,7 @@ struct ChatEventView: View {
|
||||
@State var long_press_bounce_work_item: DispatchWorkItem?
|
||||
@State var popover_state: PopoverState = .closed {
|
||||
didSet {
|
||||
let generator = UIImpactFeedbackGenerator(style: popover_state == .open_emoji_selector ? .heavy : .light)
|
||||
let generator = UIImpactFeedbackGenerator(style: popover_state.some_sheet_open() ? .heavy : .light)
|
||||
generator.impactOccurred()
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,11 @@ struct ChatEventView: View {
|
||||
enum PopoverState: String {
|
||||
case closed
|
||||
case open_emoji_selector
|
||||
case open_zap_sheet
|
||||
|
||||
func some_sheet_open() -> Bool {
|
||||
return self == .open_zap_sheet || self == .open_emoji_selector
|
||||
}
|
||||
}
|
||||
|
||||
var just_started: Bool {
|
||||
@@ -90,9 +95,22 @@ struct ChatEventView: View {
|
||||
var by_other_user: Bool {
|
||||
return event.pubkey != damus_state.pubkey
|
||||
}
|
||||
|
||||
|
||||
var is_ours: Bool { return !by_other_user }
|
||||
|
||||
|
||||
// MARK: Zapping properties
|
||||
|
||||
var lnurl: String? {
|
||||
damus_state.profiles.lookup_with_timestamp(event.pubkey)?.map({ pr in
|
||||
pr?.lnurl
|
||||
}).value
|
||||
}
|
||||
var zap_target: ZapTarget {
|
||||
ZapTarget.note(id: event.id, author: event.pubkey)
|
||||
}
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var event_bubble: some View {
|
||||
ChatBubble(
|
||||
direction: is_ours ? .right : .left,
|
||||
@@ -107,6 +125,7 @@ struct ChatEventView: View {
|
||||
.onTapGesture {
|
||||
show_profile_action_sheet_if_enabled(damus_state: damus_state, pubkey: event.pubkey)
|
||||
}
|
||||
.lineLimit(1)
|
||||
Text(verbatim: "\(format_relative_time(event.created_at))")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
@@ -124,13 +143,18 @@ struct ChatEventView: View {
|
||||
}
|
||||
|
||||
let blur_images = should_blur_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
|
||||
NoteContentView(damus_state: damus_state, event: event, blur_images: blur_images, size: .normal, options: [])
|
||||
NoteContentView(damus_state: damus_state, event: event, blur_images: blur_images, size: .normal, options: [.truncate_content])
|
||||
.padding(2)
|
||||
if let mention = first_eref_mention(ev: event, keypair: damus_state.keypair) {
|
||||
MentionView(damus_state: damus_state, mention: mention)
|
||||
.background(DamusColors.adaptableWhite)
|
||||
.clipShape(RoundedRectangle(cornerSize: CGSize(width: 10, height: 10)))
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 150, alignment: is_ours ? .trailing : .leading)
|
||||
.frame(minWidth: 5, alignment: is_ours ? .trailing : .leading)
|
||||
.padding(10)
|
||||
}
|
||||
.tint(is_ours ? Color.white : Color.accentColor)
|
||||
.tint(Color.accentColor)
|
||||
.overlay(
|
||||
ZStack(alignment: is_ours ? .bottomLeading : .bottomTrailing) {
|
||||
VStack {
|
||||
@@ -164,6 +188,14 @@ struct ChatEventView: View {
|
||||
EmojiPickerView(selectedEmoji: $selected_emoji, emojiProvider: damus_state.emoji_provider)
|
||||
}.presentationDetents([.medium, .large])
|
||||
}
|
||||
.sheet(isPresented: Binding(get: { popover_state == .open_zap_sheet }, set: { new_state in
|
||||
withAnimation(new_state == true ? .easeIn(duration: 0.5) : .easeOut(duration: 0.1)) {
|
||||
popover_state = new_state == true ? .open_zap_sheet : .closed
|
||||
}
|
||||
})) {
|
||||
ZapSheetViewIfPossible(damus_state: damus_state, target: zap_target, lnurl: lnurl)
|
||||
.presentationDetents([.medium, .large])
|
||||
}
|
||||
.onChange(of: selected_emoji) { newSelectedEmoji in
|
||||
if let newSelectedEmoji {
|
||||
send_like(emoji: newSelectedEmoji.value)
|
||||
@@ -171,8 +203,8 @@ struct ChatEventView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.scaleEffect(self.popover_state == .open_emoji_selector ? 1.08 : is_pressing ? 1.02 : 1)
|
||||
.shadow(color: (is_pressing || self.popover_state == .open_emoji_selector) ? .black.opacity(0.1) : .black.opacity(0.3), radius: (is_pressing || self.popover_state == .open_emoji_selector) ? 8 : 0, y: (is_pressing || self.popover_state == .open_emoji_selector) ? 15 : 0)
|
||||
.scaleEffect(self.popover_state.some_sheet_open() ? 1.08 : is_pressing ? 1.02 : 1)
|
||||
.shadow(color: (is_pressing || self.popover_state.some_sheet_open()) ? .black.opacity(0.1) : .black.opacity(0.3), radius: (is_pressing || self.popover_state.some_sheet_open()) ? 8 : 0, y: (is_pressing || self.popover_state.some_sheet_open()) ? 15 : 0)
|
||||
.onLongPressGesture(minimumDuration: 0.5, maximumDistance: 10, perform: {
|
||||
long_press_bounce_work_item?.cancel()
|
||||
}, onPressingChanged: { is_pressing in
|
||||
@@ -186,7 +218,8 @@ struct ChatEventView: View {
|
||||
// Ensure the action is performed only if the condition is still valid
|
||||
if self.is_pressing {
|
||||
withAnimation(.bouncy(duration: 0.2, extraBounce: 0.35)) {
|
||||
popover_state = .open_emoji_selector
|
||||
let should_show_zap_sheet = !damus_state.settings.nozaps && damus_state.settings.onlyzaps_mode
|
||||
popover_state = should_show_zap_sheet ? .open_zap_sheet : .open_emoji_selector
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,13 +287,25 @@ struct ChatEventView: View {
|
||||
SwipeView {
|
||||
self.event_bubble_with_long_press_interaction
|
||||
} leadingActions: { context in
|
||||
EventActionBar(
|
||||
damus_state: damus_state,
|
||||
event: event,
|
||||
bar: bar,
|
||||
options: is_ours ? [.swipe_action_menu_reverse] : [.swipe_action_menu],
|
||||
swipe_context: context
|
||||
)
|
||||
if !is_ours {
|
||||
EventActionBar(
|
||||
damus_state: damus_state,
|
||||
event: event,
|
||||
bar: bar,
|
||||
options: is_ours ? [.swipe_action_menu_reverse] : [.swipe_action_menu],
|
||||
swipe_context: context
|
||||
)
|
||||
}
|
||||
} trailingActions: { context in
|
||||
if is_ours {
|
||||
EventActionBar(
|
||||
damus_state: damus_state,
|
||||
event: event,
|
||||
bar: bar,
|
||||
options: is_ours ? [.swipe_action_menu_reverse] : [.swipe_action_menu],
|
||||
swipe_context: context
|
||||
)
|
||||
}
|
||||
}
|
||||
.swipeSpacing(-20)
|
||||
.swipeActionsStyle(.mask)
|
||||
@@ -322,3 +367,8 @@ extension Notification.Name {
|
||||
let bar = make_actionbar_model(ev: test_note.id, damus: test_damus_state)
|
||||
return ChatEventView(event: test_short_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: true, bar: bar)
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let bar = make_actionbar_model(ev: test_note.id, damus: test_damus_state)
|
||||
return ChatEventView(event: test_super_short_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: false, bar: bar)
|
||||
}
|
||||
|
||||
@@ -25,68 +25,44 @@ struct CreateAccountView: View {
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
VStack {
|
||||
Spacer()
|
||||
VStack(alignment: .center) {
|
||||
EditPictureControl(uploader: .nostrBuild, pubkey: account.pubkey, image_url: $account.profile_image , uploadObserver: profileUploadObserver, callback: uploadedProfilePicture)
|
||||
|
||||
Text("Public Key", comment: "Label to indicate the public key of the account.")
|
||||
|
||||
EditPictureControl(uploader: .nostrBuild, pubkey: account.pubkey, size: 75, setup: true, image_url: $account.profile_image , uploadObserver: profileUploadObserver, callback: uploadedProfilePicture)
|
||||
.shadow(radius: 2)
|
||||
.padding(.top, 100)
|
||||
|
||||
Text("Add Photo", comment: "Label to indicate user can add a photo.")
|
||||
.bold()
|
||||
.padding()
|
||||
.onTapGesture {
|
||||
regen_key()
|
||||
}
|
||||
|
||||
KeyText($account.pubkey)
|
||||
.padding(.horizontal, 20)
|
||||
.onTapGesture {
|
||||
regen_key()
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 250, alignment: .center)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(DamusColors.adaptableGrey, strokeBorder: .gray.opacity(0.5), lineWidth: 1)
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
}
|
||||
|
||||
SignupForm {
|
||||
FormLabel(NSLocalizedString("Display name", comment: "Label to prompt display name entry."), optional: true)
|
||||
FormTextInput(NSLocalizedString("Satoshi Nakamoto", comment: "Name of Bitcoin creator(s)."), text: $account.real_name)
|
||||
FormLabel(NSLocalizedString("Name", comment: "Label to prompt name entry."), optional: false)
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
FormTextInput(NSLocalizedString("Satoshi Nakamoto", comment: "Name of Bitcoin creator(s)."), text: $account.name)
|
||||
.textInputAutocapitalization(.words)
|
||||
|
||||
FormLabel(NSLocalizedString("About", comment: "Label to prompt for about text entry for user to describe about themself."), optional: true)
|
||||
FormTextInput(NSLocalizedString("Creator(s) of Bitcoin. Absolute legend.", comment: "Example description about Bitcoin creator(s), Satoshi Nakamoto."), text: $account.about)
|
||||
FormLabel(NSLocalizedString("Bio", comment: "Label to prompt bio entry for user to describe themself."), optional: true)
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
FormTextInput(NSLocalizedString("Absolute legend.", comment: "Example Bio"), text: $account.about)
|
||||
}
|
||||
.padding(.top, 10)
|
||||
.padding(.top, 25)
|
||||
|
||||
Button(action: {
|
||||
nav.push(route: Route.SaveKeys(account: account))
|
||||
}) {
|
||||
HStack {
|
||||
Text("Create account now", comment: "Button to create account.")
|
||||
Text("Next", comment: "Button to continue with account creation.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.disabled(profileUploadObserver.isLoading)
|
||||
.opacity(profileUploadObserver.isLoading ? 0.5 : 1)
|
||||
.disabled(profileUploadObserver.isLoading || account.name.isEmpty)
|
||||
.opacity(profileUploadObserver.isLoading || account.name.isEmpty ? 0.5 : 1)
|
||||
.padding(.top, 20)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Text("By signing up, you agree to our ", comment: "Ask the user if they already have an account on Nostr")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color("DamusMediumGrey"))
|
||||
|
||||
Button(action: {
|
||||
nav.push(route: Route.EULA)
|
||||
}, label: {
|
||||
Text("EULA")
|
||||
.font(.subheadline)
|
||||
})
|
||||
.padding(.vertical, 5)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
LoginPrompt()
|
||||
.padding(.top)
|
||||
|
||||
@@ -94,8 +70,8 @@ struct CreateAccountView: View {
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top)
|
||||
.dismissKeyboardOnTap()
|
||||
.navigationTitle("Create account")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarItems(leading: BackNav())
|
||||
@@ -111,7 +87,7 @@ struct LoginPrompt: View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text("Already on Nostr?", comment: "Ask the user if they already have an account on Nostr")
|
||||
.foregroundColor(Color("DamusMediumGrey"))
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
|
||||
Button(NSLocalizedString("Login", comment: "Button to navigate to login view.")) {
|
||||
self.dismiss()
|
||||
@@ -127,8 +103,8 @@ struct BackNav: View {
|
||||
var body: some View {
|
||||
Image("chevron-left")
|
||||
.foregroundColor(DamusColors.adaptableBlack)
|
||||
.onTapGesture {
|
||||
self.dismiss()
|
||||
.onTapGesture {
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,20 +124,11 @@ extension View {
|
||||
|
||||
struct CreateAccountView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let model = CreateAccountModel(real: "", nick: "jb55", about: "")
|
||||
let model = CreateAccountModel(display_name: "", name: "jb55", about: "")
|
||||
return CreateAccountView(account: model, nav: .init())
|
||||
}
|
||||
}
|
||||
|
||||
func KeyText(_ pubkey: Binding<Pubkey>) -> some View {
|
||||
let bechkey = bech32_encode(hrp: PUBKEY_HRP, pubkey.wrappedValue.bytes)
|
||||
return Text(bechkey)
|
||||
.textSelection(.enabled)
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.callout.monospaced())
|
||||
.foregroundStyle(DamusLogoGradient.gradient)
|
||||
}
|
||||
|
||||
func FormTextInput(_ title: String, text: Binding<String>) -> some View {
|
||||
return TextField("", text: text)
|
||||
.placeholder(when: text.wrappedValue.isEmpty) {
|
||||
@@ -171,6 +138,10 @@ func FormTextInput(_ title: String, text: Binding<String>) -> some View {
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(.gray.opacity(0.5), lineWidth: 1)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.foregroundColor(.damusAdaptableWhite)
|
||||
}
|
||||
}
|
||||
.font(.body.bold())
|
||||
}
|
||||
@@ -183,6 +154,10 @@ func FormLabel(_ title: String, optional: Bool = false) -> some View {
|
||||
Text("optional", comment: "Label indicating that a form input is optional.")
|
||||
.font(.callout)
|
||||
.foregroundColor(DamusColors.mediumGrey)
|
||||
} else {
|
||||
Text("required", comment: "Label indicating that a form input is required.")
|
||||
.font(.callout)
|
||||
.foregroundColor(DamusColors.mediumGrey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,13 +72,11 @@ struct DirectMessagesView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
CustomPicker(selection: $dm_type, content: {
|
||||
Text("DMs", comment: "Picker option for DM selector for seeing only DMs that have been responded to. DM is the English abbreviation for Direct Message.")
|
||||
.tag(DMType.friend)
|
||||
Text("Requests", comment: "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.")
|
||||
.tag(DMType.rando)
|
||||
})
|
||||
|
||||
CustomPicker(tabs: [
|
||||
(NSLocalizedString("DMs", comment: "Picker option for DM selector for seeing only DMs that have been responded to. DM is the English abbreviation for Direct Message."), DMType.friend),
|
||||
(NSLocalizedString("Requests", comment: "Picker option for DM selector for seeing only message requests (DMs that someone else sent the user which has not been responded to yet"), DMType.rando),
|
||||
], selection: $dm_type)
|
||||
|
||||
Divider()
|
||||
.frame(height: 1)
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ struct EventView: View {
|
||||
}
|
||||
} else if event.known_kind == .longform {
|
||||
LongformPreview(state: damus, ev: event, options: options)
|
||||
} else if event.known_kind == .highlight {
|
||||
HighlightView(state: damus, event: event, options: options)
|
||||
} else {
|
||||
TextEvent(damus: damus, event: event, pubkey: pubkey, options: options)
|
||||
//.padding([.top], 6)
|
||||
|
||||
@@ -15,10 +15,12 @@ struct ReplyPart: View {
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let reply_ref = event.thread_reply()?.reply {
|
||||
ReplyDescription(event: event, replying_to: events.lookup(reply_ref.note_id), ndb: ndb)
|
||||
} else {
|
||||
EmptyView()
|
||||
if event.known_kind == .highlight {
|
||||
let highlighted_note = event.highlighted_note_id().flatMap { events.lookup($0) }
|
||||
HighlightDescription(event: event, highlighted_event: highlighted_note, ndb: ndb)
|
||||
} else if let reply_ref = event.thread_reply()?.reply {
|
||||
let replying_to = events.lookup(reply_ref.note_id)
|
||||
ReplyDescription(event: event, replying_to: replying_to, ndb: ndb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,14 @@ struct EventBody: View {
|
||||
if !options.contains(.truncate_content) {
|
||||
note_content
|
||||
}
|
||||
} else if event.known_kind == .highlight {
|
||||
HighlightBodyView(state: damus_state, ev: event, options: options)
|
||||
.onTapGesture {
|
||||
if let highlighted_note = event.highlighted_note_id().flatMap({ damus_state.events.lookup($0) }) {
|
||||
let thread = ThreadModel(event: highlighted_note, damus_state: damus_state, highlight: event.content)
|
||||
damus_state.nav.push(route: Route.Thread(thread: thread))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
note_content
|
||||
}
|
||||
|
||||
53
damus/Views/Events/Highlight/HighlightDescription.swift
Normal file
53
damus/Views/Events/Highlight/HighlightDescription.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// HighlightDescription.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 4/28/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// Modified from Reply Description
|
||||
struct HighlightDescription: View {
|
||||
let event: NostrEvent
|
||||
let highlighted_event: NostrEvent?
|
||||
let ndb: Ndb
|
||||
|
||||
var body: some View {
|
||||
(Text(Image(systemName: "highlighter")) + Text(verbatim: " \(highlight_desc(ndb: ndb, event: event, highlighted_event: highlighted_event))"))
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct HighlightDescription_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HighlightDescription(event: test_note, highlighted_event: test_note, ndb: test_damus_state.ndb)
|
||||
}
|
||||
}
|
||||
|
||||
func highlight_desc(ndb: Ndb, event: NostrEvent, highlighted_event: NostrEvent?, locale: Locale = Locale.current) -> String {
|
||||
let desc = make_reply_description(event, replying_to: highlighted_event)
|
||||
let pubkeys = desc.pubkeys
|
||||
|
||||
let bundle = bundleForLocale(locale: locale)
|
||||
|
||||
if pubkeys.count == 0 {
|
||||
return NSLocalizedString("Highlighted", bundle: bundle, comment: "Label to indicate that the user is highlighting their own post.")
|
||||
}
|
||||
|
||||
guard let profile_txn = NdbTxn(ndb: ndb) else {
|
||||
return ""
|
||||
}
|
||||
|
||||
let names: [String] = pubkeys.map { pk in
|
||||
let prof = ndb.lookup_profile_with_txn(pk, txn: profile_txn)
|
||||
|
||||
return Profile.displayName(profile: prof?.profile, pubkey: pk).username.truncate(maxLength: 50)
|
||||
}
|
||||
|
||||
let uniqueNames: [String] = Array(Set(names))
|
||||
return String(format: NSLocalizedString("Highlighted %@", bundle: bundle, comment: "Label to indicate that the user is highlighting 1 user."), locale: locale, uniqueNames.first ?? "")
|
||||
}
|
||||
92
damus/Views/Events/Highlight/HighlightEventRef.swift
Normal file
92
damus/Views/Events/Highlight/HighlightEventRef.swift
Normal file
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// HighlightEventRef.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 4/29/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct HighlightEventRef: View {
|
||||
let damus_state: DamusState
|
||||
let event_ref: NoteId
|
||||
|
||||
init(damus_state: DamusState, event_ref: NoteId) {
|
||||
self.damus_state = damus_state
|
||||
self.event_ref = event_ref
|
||||
}
|
||||
|
||||
struct FailedImage: View {
|
||||
var body: some View {
|
||||
Image("markdown")
|
||||
.resizable()
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
.background(DamusColors.neutral3)
|
||||
.frame(width: 35, height: 35)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(.gray.opacity(0.5), lineWidth: 0.5))
|
||||
.scaledToFit()
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
EventLoaderView(damus_state: damus_state, event_id: event_ref) { event in
|
||||
EventMutingContainerView(damus_state: damus_state, event: event) {
|
||||
if event.known_kind == .longform {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
let longform_event = LongformEvent.parse(from: event)
|
||||
if let url = longform_event.image {
|
||||
KFAnimatedImage(url)
|
||||
.callbackQueue(.dispatch(.global(qos:.background)))
|
||||
.backgroundDecode(true)
|
||||
.imageContext(.note, disable_animation: true)
|
||||
.image_fade(duration: 0.25)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.background {
|
||||
FailedImage()
|
||||
}
|
||||
.frame(width: 35, height: 35)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(.gray.opacity(0.5), lineWidth: 0.5))
|
||||
.scaledToFit()
|
||||
} else {
|
||||
FailedImage()
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(longform_event.title ?? "Untitled")
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.lineLimit(1)
|
||||
|
||||
let profile_txn = damus_state.profiles.lookup(id: longform_event.event.pubkey, txn_name: "highlight-profile")
|
||||
let profile = profile_txn?.unsafeUnownedValue
|
||||
|
||||
if let display_name = profile?.display_name {
|
||||
Text(display_name)
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.gray)
|
||||
} else if let name = profile?.name {
|
||||
Text(name)
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding([.leading, .vertical], 7)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(DamusColors.neutral3, lineWidth: 2)
|
||||
)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
damus/Views/Events/Highlight/HighlightLink.swift
Normal file
101
damus/Views/Events/Highlight/HighlightLink.swift
Normal file
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// HighlightLink.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 4/28/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct HighlightLink: View {
|
||||
let state: DamusState
|
||||
let url: URL
|
||||
let content: String
|
||||
@Environment(\.openURL) var openURL
|
||||
|
||||
func text_fragment_url() -> URL? {
|
||||
let fragmentDirective = "#:~:"
|
||||
let textDirective = "text="
|
||||
let separator = ","
|
||||
var text = ""
|
||||
|
||||
let components = content.components(separatedBy: " ")
|
||||
if components.count <= 10 {
|
||||
text = content
|
||||
} else {
|
||||
let textStart = Array(components.prefix(5)).joined(separator: " ")
|
||||
let textEnd = Array(components.suffix(2)).joined(separator: " ")
|
||||
text = textStart + separator + textEnd
|
||||
}
|
||||
|
||||
let url_with_fragments = url.absoluteString + fragmentDirective + textDirective + text
|
||||
return URL(string: url_with_fragments)
|
||||
}
|
||||
|
||||
func get_url_icon() -> URL? {
|
||||
var icon = URL(string: url.absoluteString + "/favicon.ico")
|
||||
if let url_host = url.host() {
|
||||
icon = URL(string: "https://" + url_host + "/favicon.ico")
|
||||
}
|
||||
return icon
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
openURL(text_fragment_url() ?? url)
|
||||
}, label: {
|
||||
HStack(spacing: 10) {
|
||||
if let url = get_url_icon() {
|
||||
KFAnimatedImage(url)
|
||||
.imageContext(.pfp, disable_animation: true)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.placeholder { _ in
|
||||
Image("link")
|
||||
.resizable()
|
||||
.padding(5)
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
.background(DamusColors.adaptableWhite)
|
||||
}
|
||||
.frame(width: 35, height: 35)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.scaledToFit()
|
||||
} else {
|
||||
Image("link")
|
||||
.resizable()
|
||||
.padding(5)
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
.background(DamusColors.adaptableWhite)
|
||||
.frame(width: 35, height: 35)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
|
||||
Text(url.absoluteString)
|
||||
.font(eventviewsize_to_font(.normal, font_size: state.settings.font_size))
|
||||
.foregroundColor(DamusColors.adaptableBlack)
|
||||
.truncationMode(.tail)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding([.leading, .vertical], 7)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(DamusColors.neutral3)
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(DamusColors.neutral3, lineWidth: 2)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct HighlightLink_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let url = URL(string: "https://damus.io")!
|
||||
VStack {
|
||||
HighlightLink(state: test_damus_state, url: url, content: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
77
damus/Views/Events/Highlight/HighlightPostView.swift
Normal file
77
damus/Views/Events/Highlight/HighlightPostView.swift
Normal file
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// HighlightPostView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 5/26/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HighlightPostView: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
@Binding var selectedText: String
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
VStack {
|
||||
HStack(spacing: 5.0) {
|
||||
Button(action: {
|
||||
dismiss()
|
||||
}, label: {
|
||||
Text("Cancel", comment: "Button to cancel out of highlighting a note.")
|
||||
.padding(10)
|
||||
})
|
||||
.buttonStyle(NeutralButtonStyle())
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(NSLocalizedString("Post", comment: "Button to post a highlight.")) {
|
||||
let tags: [[String]] = [ ["e", "\(self.event.id)"] ]
|
||||
|
||||
let kind = NostrKind.highlight.rawValue
|
||||
guard let ev = NostrEvent(content: selectedText, keypair: damus_state.keypair, kind: kind, tags: tags) else {
|
||||
return
|
||||
}
|
||||
damus_state.postbox.send(ev)
|
||||
dismiss()
|
||||
}
|
||||
.bold()
|
||||
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||
}
|
||||
|
||||
Divider()
|
||||
.foregroundColor(DamusColors.neutral3)
|
||||
.padding(.top, 5)
|
||||
}
|
||||
.frame(height: 30)
|
||||
.padding()
|
||||
.padding(.top, 15)
|
||||
|
||||
HStack {
|
||||
var attributedString: AttributedString {
|
||||
var attributedString = AttributedString(selectedText)
|
||||
|
||||
if let range = attributedString.range(of: selectedText) {
|
||||
attributedString[range].backgroundColor = DamusColors.highlight
|
||||
}
|
||||
|
||||
return attributedString
|
||||
}
|
||||
|
||||
Text(attributedString)
|
||||
.lineSpacing(5)
|
||||
.padding(10)
|
||||
}
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 25).fill(DamusColors.highlight).frame(width: 4),
|
||||
alignment: .leading
|
||||
)
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
192
damus/Views/Events/Highlight/HighlightView.swift
Normal file
192
damus/Views/Events/Highlight/HighlightView.swift
Normal file
@@ -0,0 +1,192 @@
|
||||
//
|
||||
// HighlightView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 4/22/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct HighlightTruncatedText: View {
|
||||
let attributedString: AttributedString
|
||||
let maxChars: Int
|
||||
|
||||
init(attributedString: AttributedString, maxChars: Int = 360) {
|
||||
self.attributedString = attributedString
|
||||
self.maxChars = maxChars
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
let truncatedAttributedString: AttributedString? = attributedString.truncateOrNil(maxLength: maxChars)
|
||||
|
||||
if let truncatedAttributedString {
|
||||
Text(truncatedAttributedString)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} else {
|
||||
Text(attributedString)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
if truncatedAttributedString != nil {
|
||||
Spacer()
|
||||
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HighlightBodyView: View {
|
||||
let state: DamusState
|
||||
let event: HighlightEvent
|
||||
let options: EventViewOptions
|
||||
|
||||
init(state: DamusState, ev: HighlightEvent, options: EventViewOptions) {
|
||||
self.state = state
|
||||
self.event = ev
|
||||
self.options = options
|
||||
}
|
||||
|
||||
init(state: DamusState, ev: NostrEvent, options: EventViewOptions) {
|
||||
self.state = state
|
||||
self.event = HighlightEvent.parse(from: ev)
|
||||
self.options = options
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if options.contains(.wide) {
|
||||
Main.padding(.horizontal)
|
||||
} else {
|
||||
Main
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var truncate: Bool {
|
||||
return options.contains(.truncate_content)
|
||||
}
|
||||
|
||||
var truncate_very_short: Bool {
|
||||
return options.contains(.truncate_content_very_short)
|
||||
}
|
||||
|
||||
func truncatedText(attributedString: AttributedString) -> some View {
|
||||
Group {
|
||||
if truncate_very_short {
|
||||
HighlightTruncatedText(attributedString: attributedString, maxChars: 140)
|
||||
.font(eventviewsize_to_font(.normal, font_size: state.settings.font_size))
|
||||
}
|
||||
else if truncate {
|
||||
HighlightTruncatedText(attributedString: attributedString)
|
||||
.font(eventviewsize_to_font(.normal, font_size: state.settings.font_size))
|
||||
} else {
|
||||
Text(attributedString)
|
||||
.font(eventviewsize_to_font(.normal, font_size: state.settings.font_size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var Main: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack {
|
||||
var attributedString: AttributedString {
|
||||
var attributedString: AttributedString = ""
|
||||
if let context = event.context {
|
||||
if context.count < event.event.content.count {
|
||||
attributedString = AttributedString(event.event.content)
|
||||
} else {
|
||||
attributedString = AttributedString(context)
|
||||
}
|
||||
} else {
|
||||
attributedString = AttributedString(event.event.content)
|
||||
}
|
||||
|
||||
if let range = attributedString.range(of: event.event.content) {
|
||||
attributedString[range].backgroundColor = DamusColors.highlight
|
||||
}
|
||||
return attributedString
|
||||
}
|
||||
|
||||
truncatedText(attributedString: attributedString)
|
||||
.lineSpacing(5)
|
||||
.padding(10)
|
||||
}
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 25).fill(DamusColors.highlight).frame(width: 4),
|
||||
alignment: .leading
|
||||
)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
if let url = event.url_ref {
|
||||
HighlightLink(state: state, url: url, content: event.event.content)
|
||||
} else {
|
||||
if let evRef = event.event_ref {
|
||||
if let eventHex = hex_decode_id(evRef) {
|
||||
HighlightEventRef(damus_state: state, event_ref: NoteId(eventHex))
|
||||
.padding(.top, 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HighlightView: View {
|
||||
let state: DamusState
|
||||
let event: HighlightEvent
|
||||
let options: EventViewOptions
|
||||
|
||||
init(state: DamusState, event: NostrEvent, options: EventViewOptions) {
|
||||
self.state = state
|
||||
self.event = HighlightEvent.parse(from: event)
|
||||
self.options = options.union(.no_mentions)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
EventShell(state: state, event: event.event, options: options) {
|
||||
HighlightBodyView(state: state, ev: event, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HighlightView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
|
||||
let content = "Nostr, a decentralized and open social network protocol. Without ads, toxic algorithms, or censorship"
|
||||
let context = "Damus is built on Nostr, a decentralized and open social network protocol. Without ads, toxic algorithms, or censorship, Damus gives you access to the social network that a truly free and healthy society needs — and deserves."
|
||||
|
||||
let test_highlight_event = HighlightEvent.parse(from: NostrEvent(
|
||||
content: content,
|
||||
keypair: test_keypair,
|
||||
kind: NostrKind.highlight.rawValue,
|
||||
tags: [
|
||||
["context", context],
|
||||
["r", "https://damus.io"],
|
||||
["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"],
|
||||
])!
|
||||
)
|
||||
|
||||
let test_highlight_event2 = HighlightEvent.parse(from: NostrEvent(
|
||||
content: content,
|
||||
keypair: test_keypair,
|
||||
kind: NostrKind.highlight.rawValue,
|
||||
tags: [
|
||||
["context", context],
|
||||
["e", "36017b098859d62e1dbd802290d59c9de9f18bb0ca00ba4b875c2930dd5891ae"],
|
||||
["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"],
|
||||
])!
|
||||
)
|
||||
VStack {
|
||||
HighlightView(state: test_damus_state, event: test_highlight_event.event, options: [])
|
||||
|
||||
HighlightView(state: test_damus_state, event: test_highlight_event2.event, options: [.wide])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,10 @@ struct LongformView: View {
|
||||
var options: EventViewOptions {
|
||||
return [.wide, .no_mentions, .no_replying_to]
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
EventShell(state: state, event: event.event, options: options) {
|
||||
SelectableText(attributedString: AttributedString(stringLiteral: event.title ?? "Untitled"), size: .title)
|
||||
SelectableText(damus_state: state, event: event.event, attributedString: AttributedString(stringLiteral: event.title ?? "Untitled"), size: .title)
|
||||
|
||||
NoteContentView(damus_state: state, event: event.event, blur_images: false, size: .selected, options: options)
|
||||
}
|
||||
|
||||
@@ -39,12 +39,10 @@ struct SelectedEventView: View {
|
||||
.padding(.horizontal)
|
||||
.minimumScaleFactor(0.75)
|
||||
.lineLimit(1)
|
||||
|
||||
if let reply_ref = event.thread_reply()?.reply {
|
||||
ReplyDescription(event: event, replying_to: damus.events.lookup(reply_ref.note_id), ndb: damus.ndb)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
|
||||
ReplyPart(events: damus.events, event: event, keypair: damus.keypair, ndb: damus.ndb)
|
||||
.padding(.horizontal)
|
||||
|
||||
ProxyView(event: event)
|
||||
.padding(.top, 5)
|
||||
.padding(.horizontal)
|
||||
|
||||
@@ -160,10 +160,10 @@ struct FollowingView: View {
|
||||
.navigationBarTitle(NSLocalizedString("Following", comment: "Navigation bar title for view that shows who a user is following."))
|
||||
.safeAreaInset(edge: .top, spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
CustomPicker(selection: $tab_selection, content: {
|
||||
Text("People", comment: "Label for filter for seeing only people follows.").tag(FollowingViewTabSelection.people)
|
||||
Text("Hashtags", comment: "Label for filter for seeing only hashtag follows.").tag(FollowingViewTabSelection.hashtags)
|
||||
})
|
||||
CustomPicker(tabs: [
|
||||
(NSLocalizedString("People", comment: "Label for filter for seeing only people follows."), FollowingViewTabSelection.people),
|
||||
(NSLocalizedString("Hashtags", comment: "Label for filter for seeing only hashtag follows."), FollowingViewTabSelection.hashtags)
|
||||
], selection: $tab_selection)
|
||||
Divider()
|
||||
.frame(height: 1)
|
||||
}
|
||||
|
||||
@@ -62,8 +62,9 @@ struct LoginView: View {
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
SignInHeader()
|
||||
.padding(.top, 100)
|
||||
|
||||
SignInEntry(key: $key, shouldSaveKey: $shouldSaveKey)
|
||||
|
||||
@@ -112,8 +113,9 @@ struct LoginView: View {
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.padding(.bottom, 50)
|
||||
}
|
||||
.background(DamusBackground(maxHeight: 350), alignment: .top)
|
||||
.background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top)
|
||||
.onAppear {
|
||||
credential_handler.check_credentials()
|
||||
}
|
||||
@@ -320,9 +322,13 @@ struct KeyInput: View {
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
.padding(.horizontal, 10)
|
||||
.overlay {
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(.gray, lineWidth: 1)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.foregroundColor(.damusAdaptableWhite)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,11 +343,12 @@ struct SignInHeader: View {
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Sign in", comment: "Title of view to log into an account.")
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
.font(.system(size: 32, weight: .bold))
|
||||
.padding(.bottom, 5)
|
||||
|
||||
Text("Welcome to the social network you control", comment: "Welcome text")
|
||||
.foregroundColor(Color("DamusMediumGrey"))
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -353,6 +360,7 @@ struct SignInEntry: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Enter your account key", comment: "Prompt for user to enter an account key to login.")
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
.fontWeight(.medium)
|
||||
.padding(.top, 30)
|
||||
|
||||
@@ -444,7 +452,9 @@ struct LoginView_Previews: PreviewProvider {
|
||||
let bech32_pubkey = "KeyInput"
|
||||
Group {
|
||||
LoginView(key: pubkey, nav: .init())
|
||||
.previewDevice(PreviewDevice(rawValue: "iPhone SE (3rd generation)"))
|
||||
LoginView(key: bech32_pubkey, nav: .init())
|
||||
.previewDevice(PreviewDevice(rawValue: "iPhone 15 Pro Max"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,10 +132,10 @@ struct NoteContentView: View {
|
||||
VStack(alignment: .leading) {
|
||||
if size == .selected {
|
||||
if with_padding {
|
||||
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
|
||||
SelectableText(damus_state: damus_state, event: self.event, attributedString: artifacts.content.attributed, size: self.size)
|
||||
.padding(.horizontal)
|
||||
} else {
|
||||
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
|
||||
SelectableText(damus_state: damus_state, event: self.event, attributedString: artifacts.content.attributed, size: self.size)
|
||||
}
|
||||
} else {
|
||||
if with_padding {
|
||||
@@ -390,7 +390,12 @@ struct NoteContentView_Previews: PreviewProvider {
|
||||
NoteContentView(damus_state: state, event: test_note, blur_images: false, size: .normal, options: [])
|
||||
}
|
||||
.previewDisplayName("Short note")
|
||||
|
||||
|
||||
VStack {
|
||||
NoteContentView(damus_state: state, event: test_super_short_note, blur_images: true, size: .normal, options: [])
|
||||
}
|
||||
.previewDisplayName("Super short note")
|
||||
|
||||
VStack {
|
||||
NoteContentView(damus_state: state, event: test_encoded_note_with_image!, blur_images: false, size: .normal, options: [])
|
||||
}
|
||||
|
||||
@@ -117,17 +117,11 @@ struct NotificationsView: View {
|
||||
}
|
||||
.safeAreaInset(edge: .top, spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
CustomPicker(selection: $filter_state, content: {
|
||||
Text("All", comment: "Label for filter for all notifications.")
|
||||
.tag(NotificationFilterState.all)
|
||||
|
||||
Text("Zaps", comment: "Label for filter for zap notifications.")
|
||||
.tag(NotificationFilterState.zaps)
|
||||
|
||||
Text("Mentions", comment: "Label for filter for seeing mention notifications (replies, etc).")
|
||||
.tag(NotificationFilterState.replies)
|
||||
|
||||
})
|
||||
CustomPicker(tabs: [
|
||||
(NSLocalizedString("All", comment: "Label for filter for all notifications."), NotificationFilterState.all),
|
||||
(NSLocalizedString("Zaps", comment: "Label for filter for zap notifications."), NotificationFilterState.zaps),
|
||||
(NSLocalizedString("Mentions", comment: "Label for filter for seeing mention notifications (replies, etc)."), NotificationFilterState.replies),
|
||||
], selection: $filter_state)
|
||||
Divider()
|
||||
.frame(height: 1)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ struct AboutView: View {
|
||||
Group {
|
||||
if let about_string {
|
||||
let truncated_about = show_full_about ? about_string : about_string.truncateOrNil(maxLength: max_about_length)
|
||||
SelectableText(attributedString: truncated_about ?? about_string, textAlignment: self.text_alignment, size: .subheadline)
|
||||
SelectableText(damus_state: state, event: nil, attributedString: truncated_about ?? about_string, textAlignment: self.text_alignment, size: .subheadline)
|
||||
|
||||
if truncated_about != nil {
|
||||
if show_full_about {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
class ImageUploadingObserver: ObservableObject {
|
||||
@Published var isLoading: Bool = false
|
||||
@@ -14,6 +15,8 @@ class ImageUploadingObserver: ObservableObject {
|
||||
struct EditPictureControl: View {
|
||||
let uploader: MediaUploader
|
||||
let pubkey: Pubkey
|
||||
var size: CGFloat? = 25
|
||||
var setup: Bool? = false
|
||||
@Binding var image_url: URL?
|
||||
@ObservedObject var uploadObserver: ImageUploadingObserver
|
||||
let callback: (URL?) -> Void
|
||||
@@ -43,20 +46,53 @@ struct EditPictureControl: View {
|
||||
if uploadObserver.isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: DamusColors.purple))
|
||||
.frame(width: size, height: size)
|
||||
.padding(10)
|
||||
.background(DamusColors.white.opacity(0.7))
|
||||
.clipShape(Circle())
|
||||
.shadow(color: DamusColors.purple, radius: 15, x: 0, y: 0)
|
||||
} else if let url = image_url {
|
||||
KFAnimatedImage(url)
|
||||
.imageContext(.pfp, disable_animation: false)
|
||||
.onFailure(fallbackUrl: URL(string: robohash(pubkey)), cacheKey: url.absoluteString)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.scaledToFill()
|
||||
.frame(width: (size ?? 25) + 10, height: (size ?? 25) + 10)
|
||||
.foregroundColor(DamusColors.white)
|
||||
.clipShape(Circle())
|
||||
.overlay(Circle().stroke(.white, lineWidth: 4))
|
||||
} else {
|
||||
Image("camera")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 25, height: 25)
|
||||
.foregroundColor(DamusColors.purple)
|
||||
.padding(10)
|
||||
.background(DamusColors.white.opacity(0.7))
|
||||
.clipShape(Circle())
|
||||
.shadow(color: DamusColors.purple, radius: 15, x: 0, y: 0)
|
||||
if setup ?? false {
|
||||
Image(systemName: "person")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(DamusColors.white)
|
||||
.padding(20)
|
||||
.clipShape(Circle())
|
||||
.background {
|
||||
Circle()
|
||||
.fill(PinkGradient, strokeBorder: .white, lineWidth: 4)
|
||||
}
|
||||
|
||||
} else {
|
||||
Image("camera")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(DamusColors.purple)
|
||||
.padding(10)
|
||||
.background(DamusColors.white.opacity(0.7))
|
||||
.clipShape(Circle())
|
||||
.background {
|
||||
Circle()
|
||||
.fill(DamusColors.purple, strokeBorder: .white, lineWidth: 2)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $show_camera) {
|
||||
@@ -110,7 +146,7 @@ struct EditPictureControl_Previews: PreviewProvider {
|
||||
let observer = ImageUploadingObserver()
|
||||
ZStack {
|
||||
Color.gray
|
||||
EditPictureControl(uploader: .nostrBuild, pubkey: test_pubkey, image_url: url, uploadObserver: observer) { _ in
|
||||
EditPictureControl(uploader: .nostrBuild, pubkey: test_pubkey, size: 100, setup: false, image_url: url, uploadObserver: observer) { _ in
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,31 +125,34 @@ struct ProfileName: View {
|
||||
return
|
||||
}
|
||||
|
||||
var profile: Profile!
|
||||
var profile_txn: NdbTxn<Profile?>!
|
||||
|
||||
switch update {
|
||||
case .remote(let pubkey):
|
||||
profile_txn = damus_state.profiles.lookup(id: pubkey)
|
||||
guard let prof = profile_txn.unsafeUnownedValue else { return }
|
||||
profile = prof
|
||||
guard let profile_txn = damus_state.profiles.lookup(id: pubkey),
|
||||
let prof = profile_txn.unsafeUnownedValue else {
|
||||
return
|
||||
}
|
||||
handle_profile_update(profile: prof)
|
||||
case .manual(_, let prof):
|
||||
profile = prof
|
||||
handle_profile_update(profile: prof)
|
||||
}
|
||||
|
||||
let display_name = Profile.displayName(profile: profile, pubkey: pubkey)
|
||||
if self.display_name != display_name {
|
||||
self.display_name = display_name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let nip05 = damus_state.profiles.is_validated(pubkey)
|
||||
if nip05 != self.nip05 {
|
||||
self.nip05 = nip05
|
||||
}
|
||||
@MainActor
|
||||
func handle_profile_update(profile: Profile) {
|
||||
let display_name = Profile.displayName(profile: profile, pubkey: pubkey)
|
||||
if self.display_name != display_name {
|
||||
self.display_name = display_name
|
||||
}
|
||||
|
||||
if donation != profile.damus_donation {
|
||||
donation = profile.damus_donation
|
||||
}
|
||||
let nip05 = damus_state.profiles.is_validated(pubkey)
|
||||
if nip05 != self.nip05 {
|
||||
self.nip05 = nip05
|
||||
}
|
||||
|
||||
if donation != profile.damus_donation {
|
||||
donation = profile.damus_donation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ struct ProfileView: View {
|
||||
@State var show_share_sheet: Bool = false
|
||||
@State var show_qr_code: Bool = false
|
||||
@State var action_sheet_presented: Bool = false
|
||||
@State var mute_dialog_presented: Bool = false
|
||||
@State var filter_state : FilterState = .posts
|
||||
@State var yOffset: CGFloat = 0
|
||||
|
||||
@@ -162,7 +163,10 @@ struct ProfileView: View {
|
||||
Button(action: {
|
||||
action_sheet_presented = true
|
||||
}) {
|
||||
navImage(img: "share3")
|
||||
Image(systemName: "ellipsis")
|
||||
.frame(width: 33, height: 33)
|
||||
.background(Color.black.opacity(0.6))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or mute a profile."), isPresented: $action_sheet_presented) {
|
||||
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
|
||||
@@ -196,25 +200,21 @@ struct ProfileView: View {
|
||||
damus_state.postbox.send(new_ev)
|
||||
}
|
||||
} else {
|
||||
MuteDurationMenu { duration in
|
||||
notify(.mute(.user(profile.pubkey, duration?.date_from_now)))
|
||||
} label: {
|
||||
Text("Mute", comment: "Button to mute a profile.")
|
||||
.foregroundStyle(.red)
|
||||
Button(NSLocalizedString("Mute", comment: "Button to mute a profile"), role: .destructive) {
|
||||
mute_dialog_presented = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var customNavbar: some View {
|
||||
HStack {
|
||||
navBackButton
|
||||
Spacer()
|
||||
navActionSheetButton
|
||||
.confirmationDialog(NSLocalizedString("Mute", comment: "Title for confirmation dialog to mute a profile."), isPresented: $mute_dialog_presented) {
|
||||
ForEach(DamusDuration.allCases, id: \.self) { duration in
|
||||
Button {
|
||||
notify(.mute(.user(profile.pubkey, duration.date_from_now)))
|
||||
} label: {
|
||||
Text(duration.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 5)
|
||||
.accentColor(DamusColors.white)
|
||||
}
|
||||
|
||||
func lnButton(unownedProfile: Profile?, record: ProfileRecord?) -> some View {
|
||||
@@ -424,10 +424,10 @@ struct ProfileView: View {
|
||||
aboutSection
|
||||
|
||||
VStack(spacing: 0) {
|
||||
CustomPicker(selection: $filter_state, content: {
|
||||
Text("Notes", comment: "Label for filter for seeing only your notes (instead of notes and replies).").tag(FilterState.posts)
|
||||
Text("Notes & Replies", comment: "Label for filter for seeing your notes and replies (instead of only your notes).").tag(FilterState.posts_and_replies)
|
||||
})
|
||||
CustomPicker(tabs: [
|
||||
(NSLocalizedString("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies)."), FilterState.posts),
|
||||
(NSLocalizedString("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes)."), FilterState.posts_and_replies)
|
||||
], selection: $filter_state)
|
||||
Divider()
|
||||
.frame(height: 1)
|
||||
}
|
||||
@@ -448,8 +448,15 @@ struct ProfileView: View {
|
||||
.navigationTitle("")
|
||||
.navigationBarBackButtonHidden()
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
customNavbar
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
navBackButton
|
||||
.padding(.top, 5)
|
||||
.accentColor(DamusColors.white)
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
navActionSheetButton
|
||||
.padding(.top, 5)
|
||||
.accentColor(DamusColors.white)
|
||||
}
|
||||
}
|
||||
.toolbarBackground(.hidden)
|
||||
|
||||
@@ -16,7 +16,7 @@ struct ReactionsView: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach(model.events.events, id: \.id) { ev in
|
||||
ForEach(model.events.events.filter { $0.last_refid() == model.target }, id: \.id) { ev in
|
||||
ReactionView(damus_state: damus_state, reaction: ev)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ import Security
|
||||
struct SaveKeysView: View {
|
||||
let account: CreateAccountModel
|
||||
let pool: RelayPool = RelayPool(ndb: Ndb()!)
|
||||
@State var pub_copied: Bool = false
|
||||
@State var priv_copied: Bool = false
|
||||
@State var loading: Bool = false
|
||||
@State var error: String? = nil
|
||||
|
||||
@@ -31,81 +29,98 @@ struct SaveKeysView: View {
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
VStack(alignment: .center) {
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("logo-nobg")
|
||||
.resizable()
|
||||
.shadow(color: DamusColors.purple, radius: 2)
|
||||
.frame(width: 56, height: 56, alignment: .center)
|
||||
.padding(.top, 20.0)
|
||||
|
||||
if account.rendered_name.isEmpty {
|
||||
Text("Welcome!", comment: "Text to welcome user.")
|
||||
.font(.title.bold())
|
||||
.padding(.bottom, 10)
|
||||
.font(.title)
|
||||
.fontWeight(.heavy)
|
||||
.foregroundStyle(DamusLogoGradient.gradient)
|
||||
} else {
|
||||
Text("Welcome, \(account.rendered_name)!", comment: "Text to welcome user.")
|
||||
.font(.title.bold())
|
||||
.padding(.bottom, 10)
|
||||
.font(.title)
|
||||
.fontWeight(.heavy)
|
||||
.foregroundStyle(DamusLogoGradient.gradient)
|
||||
}
|
||||
|
||||
Text("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.", comment: "Reminder to user that they should save their account information.")
|
||||
.padding(.bottom, 10)
|
||||
Text("Save your login info?", comment: "Ask user if they want to save their account information.")
|
||||
.font(.title)
|
||||
.fontWeight(.heavy)
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
.padding(.top, 5)
|
||||
|
||||
Text("Private Key", comment: "Label to indicate that the text below is the user's private key used by only the user themself as a secret to login to access their account.")
|
||||
.font(.title2.bold())
|
||||
.padding(.bottom, 10)
|
||||
Text("We'll save your account key, so you won't need to enter it manually next time you log in.", comment: "Reminder to user that they should save their account information.")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
.padding(.top, 2)
|
||||
.padding(.bottom, 100)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text("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!", comment: "Label to describe that a private key is the user's secret account key and what they should do with it.")
|
||||
.padding(.bottom, 10)
|
||||
Spacer()
|
||||
|
||||
SaveKeyView(text: account.privkey.nsec, textContentType: .newPassword, is_copied: $priv_copied, focus: $privkey_focused)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
if priv_copied {
|
||||
if loading {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
} else if let err = error {
|
||||
Text("Error: \(err)", comment: "Error message indicating why saving keys failed.")
|
||||
.foregroundColor(.red)
|
||||
|
||||
Button(action: {
|
||||
complete_account_creation(account)
|
||||
}) {
|
||||
HStack {
|
||||
Text("Retry", comment: "Button to retry completing account creation after an error occurred.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||
if loading {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
} else if let err = error {
|
||||
Text("Error: \(err)", comment: "Error message indicating why saving keys failed.")
|
||||
.foregroundColor(.red)
|
||||
|
||||
Button(action: {
|
||||
complete_account_creation(account)
|
||||
}) {
|
||||
HStack {
|
||||
Text("Retry", comment: "Button to retry completing account creation after an error occurred.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.padding(.top, 20)
|
||||
} else {
|
||||
Button(action: {
|
||||
complete_account_creation(account)
|
||||
}) {
|
||||
HStack {
|
||||
Text("Let's go!", comment: "Button to complete account creation and start using the app.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.padding(.top, 20)
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.padding(.top, 20)
|
||||
} else {
|
||||
|
||||
Button(action: {
|
||||
save_key(account)
|
||||
complete_account_creation(account)
|
||||
}) {
|
||||
HStack {
|
||||
Text("Save", comment: "Button to save key, complete account creation, and start using the app.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.padding(.top, 20)
|
||||
|
||||
Button(action: {
|
||||
complete_account_creation(account)
|
||||
}) {
|
||||
HStack {
|
||||
Text("Not now", comment: "Button to not save key, complete account creation, and start using the app.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||
}
|
||||
.buttonStyle(NeutralButtonStyle(padding: EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15), cornerRadius: 12))
|
||||
.padding(.top, 20)
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
.background(
|
||||
Image("eula-bg")
|
||||
.resizable()
|
||||
.blur(radius: 70)
|
||||
.ignoresSafeArea(),
|
||||
alignment: .top
|
||||
)
|
||||
.background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarItems(leading: BackNav())
|
||||
.onAppear {
|
||||
// Hack to force keyboard to show up for a short moment and then hiding it to register password autofill flow.
|
||||
pubkey_focused = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
pubkey_focused = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func save_key(_ account: CreateAccountModel) {
|
||||
credential_handler.save_credential(pubkey: account.pubkey, privkey: account.privkey)
|
||||
}
|
||||
|
||||
func complete_account_creation(_ account: CreateAccountModel) {
|
||||
@@ -122,8 +137,6 @@ struct SaveKeysView: View {
|
||||
}
|
||||
|
||||
self.pool.register_handler(sub_id: "signup", handler: handle_event)
|
||||
|
||||
credential_handler.save_credential(pubkey: account.pubkey, privkey: account.privkey)
|
||||
|
||||
self.loading = true
|
||||
|
||||
@@ -188,74 +201,13 @@ struct SaveKeysView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct SaveKeyView: View {
|
||||
let text: String
|
||||
let textContentType: UITextContentType
|
||||
@Binding var is_copied: Bool
|
||||
var focus: FocusState<Bool>.Binding
|
||||
|
||||
func copy_text() {
|
||||
UIPasteboard.general.string = text
|
||||
is_copied = true
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
spacerBlock(width: 0, height: 0)
|
||||
Button(action: copy_text) {
|
||||
Label("", image: is_copied ? "check-circle.fill" : "copy2")
|
||||
.foregroundColor(is_copied ? .green : .gray)
|
||||
.background {
|
||||
if is_copied {
|
||||
Circle()
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 25, height: 25, alignment: .center)
|
||||
.padding(.leading, -8)
|
||||
.padding(.top, 1)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextField("", text: .constant(text))
|
||||
.padding(5)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 4.0).opacity(0.1)
|
||||
}
|
||||
.textSelection(.enabled)
|
||||
.font(.callout.monospaced())
|
||||
.onTapGesture {
|
||||
copy_text()
|
||||
// Hack to force keyboard to hide. Showing keyboard on text field is necessary to register password autofill flow but the text itself should not be modified.
|
||||
DispatchQueue.main.async {
|
||||
end_editing()
|
||||
}
|
||||
}
|
||||
.textContentType(textContentType)
|
||||
.deleteDisabled(true)
|
||||
.focused(focus)
|
||||
|
||||
spacerBlock(width: 0, height: 0) /// set a 'width' > 0 here to vary key Text's aspect ratio
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func spacerBlock(width: CGFloat, height: CGFloat) -> some View {
|
||||
Color.orange.opacity(1)
|
||||
.frame(width: width, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
struct SaveKeysView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let model = CreateAccountModel(real: "William", nick: "jb55", about: "I'm me")
|
||||
let model = CreateAccountModel(display_name: "William", name: "jb55", about: "I'm me")
|
||||
SaveKeysView(account: model)
|
||||
}
|
||||
}
|
||||
|
||||
func create_account_to_metadata(_ model: CreateAccountModel) -> Profile {
|
||||
return Profile(name: model.nick_name, display_name: model.real_name, about: model.about, picture: model.profile_image?.absoluteString, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nil, damus_donation: nil)
|
||||
return Profile(name: model.name, display_name: model.display_name, about: model.about, picture: model.profile_image?.absoluteString, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nil, damus_donation: nil)
|
||||
}
|
||||
|
||||
@@ -28,32 +28,53 @@ struct SetupView: View {
|
||||
.fontWeight(.heavy)
|
||||
.foregroundStyle(DamusLogoGradient.gradient)
|
||||
|
||||
Text("The go-to iOS Nostr client", comment: "Quick description of what Damus is")
|
||||
.foregroundColor(DamusColors.mediumGrey)
|
||||
Text("The social network you control", comment: "Quick description of what Damus is")
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
.padding(.top, 10)
|
||||
|
||||
WhatIsNostr()
|
||||
.padding()
|
||||
|
||||
WhyWeNeedNostr()
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
navigationCoordinator.push(route: Route.CreateAccount)
|
||||
}) {
|
||||
HStack {
|
||||
Text("Create Account", comment: "Button to continue to the create account page.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.padding(.horizontal)
|
||||
|
||||
Button(action: {
|
||||
navigationCoordinator.push(route: Route.Login)
|
||||
}) {
|
||||
HStack {
|
||||
Text("Let's get started!", comment: "Button to continue to login page.")
|
||||
Text("Sign In", comment: "Button to continue to login page.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.padding()
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Text("By continuing you agree to our ")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
|
||||
Button(action: {
|
||||
navigationCoordinator.push(route: Route.EULA)
|
||||
}, label: {
|
||||
Text("EULA", comment: "End User License Agreement")
|
||||
.font(.subheadline)
|
||||
})
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
.background(DamusBackground(maxHeight: 300), alignment: .top)
|
||||
.background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top)
|
||||
.navigationDestination(for: Route.self) { route in
|
||||
route.view(navigationCoordinator: navigationCoordinator, damusState: DamusState.empty)
|
||||
}
|
||||
@@ -63,61 +84,6 @@ struct SetupView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct LearnAboutNostrLink: View {
|
||||
@Environment(\.openURL) var openURL
|
||||
var body: some View {
|
||||
HStack {
|
||||
Button(action: {
|
||||
openURL(URL(string: "https://nostr.com")!)
|
||||
}, label: {
|
||||
Text("Learn more about Nostr", comment: "Button that opens up a webpage where the user can learn more about Nostr.")
|
||||
.foregroundColor(.accentColor)
|
||||
})
|
||||
|
||||
Image(systemName: "arrow.up.right")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WhatIsNostr: View {
|
||||
var body: some View {
|
||||
HStack(alignment: .top) {
|
||||
Image("nostr-logo")
|
||||
VStack(alignment: .leading) {
|
||||
Text("What is Nostr?", comment: "Heading text for section describing what is Nostr.")
|
||||
.fontWeight(.bold)
|
||||
.padding(.vertical, 10)
|
||||
|
||||
Text("Nostr is a protocol, designed for simplicity, that aims to create a censorship-resistant global social network", comment: "Description about what is Nostr.")
|
||||
.foregroundColor(DamusColors.mediumGrey)
|
||||
|
||||
LearnAboutNostrLink()
|
||||
.padding(.top, 10)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WhyWeNeedNostr: View {
|
||||
var body: some View {
|
||||
HStack(alignment: .top) {
|
||||
Image("lightbulb")
|
||||
VStack(alignment: .leading) {
|
||||
Text("Why we need Nostr?", comment: "Heading text for section describing why Nostr is needed.")
|
||||
.fontWeight(.bold)
|
||||
.padding(.vertical, 10)
|
||||
|
||||
Text("Social media has developed into a key way information flows around the world. Unfortunately, our current social media systems are broken", comment: "Description about why Nostr is needed.")
|
||||
.foregroundColor(DamusColors.mediumGrey)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SetupView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
|
||||
80
damus/Views/Timeline/PostingTimelineView.swift
Normal file
80
damus/Views/Timeline/PostingTimelineView.swift
Normal file
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// PostingTimelineView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 7/15/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PostingTimelineView: View {
|
||||
|
||||
let damus_state: DamusState
|
||||
var home: HomeModel
|
||||
@State var search: String = ""
|
||||
@State var results: [NostrEvent] = []
|
||||
@State var initialOffset: CGFloat?
|
||||
@State var offset: CGFloat?
|
||||
@State var showSearch: Bool = true
|
||||
@Binding var active_sheet: Sheets?
|
||||
@FocusState private var isSearchFocused: Bool
|
||||
@State private var contentOffset: CGFloat = 0
|
||||
@State private var indicatorWidth: CGFloat = 0
|
||||
@State private var indicatorPosition: CGFloat = 0
|
||||
@SceneStorage("PostingTimelineView.filter_state") var filter_state : FilterState = .posts_and_replies
|
||||
|
||||
var mystery: some View {
|
||||
Text("Are you lost?", comment: "Text asking the user if they are lost in the app.")
|
||||
.id("what")
|
||||
}
|
||||
|
||||
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
|
||||
var filters = ContentFilters.defaults(damus_state: damus_state)
|
||||
filters.append(fstate.filter)
|
||||
return ContentFilters(filters: filters).filter
|
||||
}
|
||||
|
||||
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
|
||||
TimelineView(events: home.events, loading: .constant(false), damus: damus_state, show_friend_icon: false, filter: filter) {
|
||||
PullDownSearchView(state: damus_state, on_cancel: {})
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ZStack {
|
||||
TabView(selection: $filter_state) {
|
||||
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
|
||||
mystery
|
||||
|
||||
contentTimelineView(filter: content_filter(.posts))
|
||||
.tag(FilterState.posts)
|
||||
.id(FilterState.posts)
|
||||
contentTimelineView(filter: content_filter(.posts_and_replies))
|
||||
.tag(FilterState.posts_and_replies)
|
||||
.id(FilterState.posts_and_replies)
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
|
||||
if damus_state.keypair.privkey != nil {
|
||||
PostButtonContainer(is_left_handed: damus_state.settings.left_handed) {
|
||||
self.active_sheet = .post(.posting(.none))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .top, spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
CustomPicker(tabs: [
|
||||
(NSLocalizedString("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies)."), FilterState.posts),
|
||||
(NSLocalizedString("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes)."), FilterState.posts_and_replies)
|
||||
],
|
||||
selection: $filter_state)
|
||||
|
||||
Divider()
|
||||
.frame(height: 1)
|
||||
}
|
||||
.background(DamusColors.adaptableWhite)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,6 +295,64 @@ struct CustomizeZapView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct ZapSheetViewIfPossible: View {
|
||||
let damus_state: DamusState
|
||||
let target: ZapTarget
|
||||
let lnurl: String?
|
||||
var zap_sheet: ZapSheet? {
|
||||
guard let lnurl else { return nil }
|
||||
return ZapSheet(target: target, lnurl: lnurl)
|
||||
}
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var body: some View {
|
||||
if let zap_sheet {
|
||||
CustomizeZapView(state: damus_state, target: zap_sheet.target, lnurl: zap_sheet.lnurl)
|
||||
}
|
||||
else {
|
||||
zap_sheet_not_possible
|
||||
}
|
||||
}
|
||||
|
||||
var zap_sheet_not_possible: some View {
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
Image(systemName: "bolt.trianglebadge.exclamationmark.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 70)
|
||||
Text("User not zappable", comment: "Headline indicating a user cannot be zapped")
|
||||
.font(.headline)
|
||||
Text("This user cannot be zapped because they have not configured zaps on their account yet. Time to orange-pill?", comment: "Comment explaining why a user cannot be zapped.")
|
||||
.multilineTextAlignment(.center)
|
||||
.opacity(0.6)
|
||||
self.dm_button
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
var dm_button: some View {
|
||||
let dm_model = damus_state.dms.lookup_or_create(target.pubkey)
|
||||
return VStack(alignment: .center, spacing: 10) {
|
||||
Button(
|
||||
action: {
|
||||
damus_state.nav.push(route: Route.DMChat(dms: dm_model))
|
||||
dismiss()
|
||||
},
|
||||
label: {
|
||||
Image("messages")
|
||||
.profile_button_style(scheme: colorScheme)
|
||||
}
|
||||
)
|
||||
.buttonStyle(NeutralButtonShape.circle.style)
|
||||
Text("Orange-pill", comment: "Button label that allows the user to start a direct message conversation with the user shown on-screen, to orange-pill them (i.e. help them to setup zaps)")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func hideKeyboard() {
|
||||
let resign = #selector(UIResponder.resignFirstResponder)
|
||||
@@ -302,9 +360,25 @@ extension View {
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomizeZapView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CustomizeZapView(state: test_damus_state, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), lnurl: "")
|
||||
.frame(width: 400, height: 600)
|
||||
}
|
||||
|
||||
|
||||
fileprivate func test_zap_sheet() -> ZapSheet {
|
||||
let zap_target = ZapTarget.note(id: test_note.id, author: test_note.pubkey)
|
||||
let lnurl = ""
|
||||
return ZapSheet(target: zap_target, lnurl: lnurl)
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CustomizeZapView(state: test_damus_state, target: test_zap_sheet().target, lnurl: test_zap_sheet().lnurl)
|
||||
.frame(width: 400, height: 600)
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ZapSheetViewIfPossible(damus_state: test_damus_state, target: test_zap_sheet().target, lnurl: test_zap_sheet().lnurl)
|
||||
.frame(width: 400, height: 600)
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ZapSheetViewIfPossible(damus_state: test_damus_state, target: test_zap_sheet().target, lnurl: nil)
|
||||
.frame(width: 400, height: 600)
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func generate_test_damus_state(
|
||||
video: .init(),
|
||||
ndb: ndb,
|
||||
quote_reposts: .init(our_pubkey: our_pubkey),
|
||||
emoji_provider: DefaultEmojiProvider()
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: false)
|
||||
)
|
||||
|
||||
return damus
|
||||
|
||||
@@ -14,7 +14,7 @@ extension NdbNote {
|
||||
}
|
||||
|
||||
func get_cached_inner_event(cache: EventCache) -> NdbNote? {
|
||||
guard self.known_kind == .boost else {
|
||||
guard self.known_kind == .boost || self.known_kind == .highlight else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ class NdbNote: Encodable, Equatable, Hashable {
|
||||
// Extension to make NdbNote compatible with NostrEvent's original API
|
||||
extension NdbNote {
|
||||
var is_textlike: Bool {
|
||||
return kind == 1 || kind == 42 || kind == 30023
|
||||
return kind == 1 || kind == 42 || kind == 30023 || kind == 9802
|
||||
}
|
||||
|
||||
var is_quote_repost: NoteId? {
|
||||
@@ -341,7 +341,14 @@ extension NdbNote {
|
||||
}
|
||||
|
||||
func thread_reply() -> ThreadReply? {
|
||||
ThreadReply(tags: self.tags)
|
||||
if self.known_kind != .highlight {
|
||||
return ThreadReply(tags: self.tags)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func highlighted_note_id() -> NoteId? {
|
||||
return ThreadReply(tags: self.tags)?.reply.note_id
|
||||
}
|
||||
|
||||
func get_content(_ keypair: Keypair) -> String {
|
||||
|
||||
@@ -82,7 +82,7 @@ extern "C" {
|
||||
* Note: some internal assertion will remain if disabled.
|
||||
*/
|
||||
#ifndef FLATCC_BUILDER_ASSERT_ON_ERROR
|
||||
#define FLATCC_BUILDER_ASSERT_ON_ERROR 1
|
||||
#define FLATCC_BUILDER_ASSERT_ON_ERROR 0
|
||||
#endif
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user