Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gif: Nostr Build GIF Keyboard #2387

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions damus.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,8 @@
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 */; };
5C8711E12C6D8912007879C2 /* NostrBuildGIF.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8711E02C6D8912007879C2 /* NostrBuildGIF.swift */; };
5C8711E42C6D8C86007879C2 /* NostrBuildGIFGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8711E32C6D8C86007879C2 /* NostrBuildGIFGrid.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 */; };
Expand Down Expand Up @@ -1350,6 +1352,8 @@
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>"; };
5C8711E02C6D8912007879C2 /* NostrBuildGIF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrBuildGIF.swift; sourceTree = "<group>"; };
5C8711E32C6D8C86007879C2 /* NostrBuildGIFGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrBuildGIFGrid.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>"; };
Expand Down Expand Up @@ -1623,6 +1627,7 @@
4C0A3F8D280F63FF000448DE /* Models */ = {
isa = PBXGroup;
children = (
5C8711DF2C6D88F6007879C2 /* GIFs */,
D74F43082B23F09300425B75 /* Purple */,
BA3759882ABCCDE30018D73B /* Camera */,
4C190F1E2A535FC200027FD5 /* Zaps */,
Expand Down Expand Up @@ -2023,6 +2028,7 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
5C8711E22C6D8C6E007879C2 /* GIFs */,
D78DB85D2C20FE9E00F0AB12 /* Chat */,
D71AC4CA2BA8E3320076268E /* Extensions */,
BA3759952ABCCF360018D73B /* Camera */,
Expand Down Expand Up @@ -2716,6 +2722,22 @@
path = Images;
sourceTree = "<group>";
};
5C8711DF2C6D88F6007879C2 /* GIFs */ = {
isa = PBXGroup;
children = (
5C8711E02C6D8912007879C2 /* NostrBuildGIF.swift */,
);
path = GIFs;
sourceTree = "<group>";
};
5C8711E22C6D8C6E007879C2 /* GIFs */ = {
isa = PBXGroup;
children = (
5C8711E32C6D8C86007879C2 /* NostrBuildGIFGrid.swift */,
);
path = GIFs;
sourceTree = "<group>";
};
5CC852A02BDED9970039FFC5 /* Highlight */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -3426,6 +3448,7 @@
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */,
4CC14FF52A740BB7007AEB17 /* NoteId.swift in Sources */,
4C19AE512A5CEF7C00C90DB7 /* NostrScript.swift in Sources */,
5C8711E42C6D8C86007879C2 /* NostrBuildGIFGrid.swift in Sources */,
4C32B95E2A9AD44700DC3548 /* FlatBufferObject.swift in Sources */,
D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */,
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
Expand Down Expand Up @@ -3477,6 +3500,7 @@
D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */,
3165648B295B70D500C64604 /* LinkView.swift in Sources */,
4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */,
5C8711E12C6D8912007879C2 /* NostrBuildGIF.swift in Sources */,
D7CB5D5C2B1176B200AD4105 /* MediaUploader.swift in Sources */,
4C1253562A76C8C60004F4B8 /* BroadcastNotify.swift in Sources */,
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */,
Expand Down
12 changes: 12 additions & 0 deletions damus/Assets.xcassets/nostrbuild.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "nb-logo_nb-logo-color.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 62 additions & 0 deletions damus/Models/GIFs/NostrBuildGIF.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// NostrBuildGIF.swift
// damus
//
// Created by eric on 8/14/24.
//

import Foundation

let pageSize: Int = 30

func makeGIFRequest(cursor: Int) async throws -> NostrBuildGIFResponse {
var request = URLRequest(url: URL(string: String(format: "https://nostr.build/api/v2/gifs/get?cursor=%d&limit=%d&random=%d",
cursor,
pageSize,
0))!)

request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

let response: NostrBuildGIFResponse = try await decodedData(for: request)
return response
}

private func decodedData<Output: Decodable>(for request: URLRequest) async throws -> Output {
let decoder = JSONDecoder()
let session = URLSession.shared
let (data, response) = try await session.data(for: request)

if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200:
let result = try decoder.decode(Output.self, from: data)
return result
default:
Log.error("Error retrieving gif data from Nostr Build. HTTP status code: %d; Response: %s", for: .gif_request, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
throw NostrBuildError.http_response_error(status_code: httpResponse.statusCode, response: data)
}
}

throw NostrBuildError.could_not_process_response
}

enum NostrBuildError: Error {
case http_response_error(status_code: Int, response: Data)
case could_not_process_response
}

struct NostrBuildGIFResponse: Codable {
let status: String
let message: String
let cursor: Int
let count: Int
let gifs: [NostrBuildGif]
}

struct NostrBuildGif: Codable, Identifiable {
var id: String { bh }
var url: String
/// This is the blurhash of the gif that can be used as an ID and placeholder
let bh: String
ericholguin marked this conversation as resolved.
Show resolved Hide resolved
}
1 change: 1 addition & 0 deletions damus/Util/Log.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ enum LogCategory: String {
case push_notifications
case damus_purple
case image_uploading
case gif_request
}

/// Damus structured logger
Expand Down
163 changes: 163 additions & 0 deletions damus/Views/GIFs/NostrBuildGIFGrid.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//
// NostrBuildGIFGrid.swift
// damus
//
// Created by eric on 8/14/24.
//

import SwiftUI
import Kingfisher


struct NostrBuildGIFGrid: View {
let damus_state: DamusState
@State var results:[NostrBuildGif] = []
@State var cursor: Int = 0
@State var errorAlert: Bool = false
@SceneStorage("NostrBuildGIFGrid.show_nsfw_alert") var show_nsfw_alert : Bool = true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to persist show_nsfw_alert?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this way users are not presented with the alert again.

@SceneStorage("NostrBuildGIFGrid.persist_nsfw_alert") var persist_nsfw_alert : Bool = true
@Environment(\.dismiss) var dismiss

var onSelect:(String) -> ()

let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]

var TopBar: some View {
VStack {
HStack(spacing: 5.0) {

Button(action: {
Task {
cursor -= pageSize
do {
let response = try await makeGIFRequest(cursor: cursor)
self.results = response.gifs
} catch {
print(error.localizedDescription)
}
}
}, label: {
Text("Back", comment: "Button to go to previous page.")
.padding(10)
})
.buttonStyle(NeutralButtonStyle())
.opacity(cursor > 0 ? 1 : 0)
.disabled(cursor == 0)

Spacer()

Image("nostrbuild")
.resizable()
.frame(width: 40, height: 40)

Spacer()

Button(NSLocalizedString("Next", comment: "Button to go to next page.")) {
Task {
cursor += pageSize
do {
let response = try await makeGIFRequest(cursor: cursor)
self.results = response.gifs
} catch {
print(error.localizedDescription)
}
}
}
.bold()
.buttonStyle(GradientButtonStyle(padding: 10))
}

Divider()
.foregroundColor(DamusColors.neutral3)
.padding(.top, 5)
}
.frame(height: 30)
.padding()
.padding(.top, 15)
}

var body: some View {
VStack {
TopBar
ScrollView {
LazyVGrid(columns: columns, spacing: 5) {
ForEach($results) { gifResult in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personal opinion/optional comment: Instead of having results as an array that starts out empty and later gets filled up, and a separate errorAlert state, I would create a single local enum to track the state, with cases like "loading", "failure", "loaded(results: [NostrBuildGif])".

This helps differentiate the UI when there are no results vs when we are still loading results. It also simplifies state management, as an error on the NostrBuild request is interconnected with the GIF results

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, I might need some help with that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can help with that if you need anything, just let me know what you would need help with!

VStack {
if let url = URL(string: gifResult.url.wrappedValue) {
ZStack {
KFAnimatedImage(url)
.imageContext(.note, disable_animation: damus_state.settings.disable_animation)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.clipShape(RoundedRectangle(cornerRadius: 12.0))
.frame(width: 120, height: 120)
.aspectRatio(contentMode: .fill)
.onTapGesture {
onSelect(url.absoluteString)
dismiss()
}
if persist_nsfw_alert {
Blur()
}
}
}
}
}
}
Spacer()
}
}
.padding()
.alert("Error", isPresented: $errorAlert) {
Button(NSLocalizedString("OK", comment: "Exit this view")) {
dismiss()
}
} message: {
Text("Failed to load GIFs")
}
.alert("NSFW", isPresented: $show_nsfw_alert) {
Button(NSLocalizedString("Cancel", comment: "Exit this view")) {
dismiss()
}
Button(NSLocalizedString("Proceed", comment: "Button to continue")) {
show_nsfw_alert = false
persist_nsfw_alert = false
}
} message: {
Text("NSFW means \"Not Safe For Work\". The content in this view may be inappropriate to view in some situations and may contain explicit images.", comment: "Warning to the user that there may be content that is not safe for work.")
}
.onAppear {
Task {
await initial()
}
if persist_nsfw_alert {
show_nsfw_alert = true
}
}
}

func initial() async {
do {
let response = try await makeGIFRequest(cursor: cursor)
self.results = response.gifs
} catch {
print(error)
errorAlert = true
}

}
}

struct NostrBuildGIFGrid_Previews: PreviewProvider {
static var previews: some View {
NostrBuildGIFGrid(damus_state: test_damus_state) { gifURL in
print("GIF URL: \(gifURL)")
}
}
}
Loading