Skip to content

Commit

Permalink
gif: Nostr Build GIF Keyboard
Browse files Browse the repository at this point in the history
This PR adds a gif keyboard, kind of, to the posting view.
Leverages the nostr build API to get latest gifs.

Changelog-Added: Nostr Build GIF keyboard

Signed-off-by: ericholguin <[email protected]>
  • Loading branch information
ericholguin committed Sep 12, 2024
1 parent 49c8d63 commit 5caec85
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 2 deletions.
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
}
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
@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
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

0 comments on commit 5caec85

Please sign in to comment.