feat: feedback with photo support + web dashboard feedback button
All checks were successful
Security Checks / dependency-audit (push) Successful in 15s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 5s

iOS:
- Photo picker in feedback sheet (screenshot/photo attachment)
- Image sent as base64, uploaded to Gitea issue as attachment

Web:
- Feedback button in sidebar rail
- Modal with text area + send
- Auto-labels same as iOS

Gateway:
- Multipart image upload to Gitea issue assets API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-03 13:31:20 -05:00
parent 557bd80174
commit 96fa49fae2
4 changed files with 198 additions and 4 deletions

View File

@@ -1,4 +1,5 @@
import SwiftUI
import PhotosUI
struct FeedbackButton: View {
@State private var showSheet = false
@@ -15,7 +16,7 @@ struct FeedbackButton: View {
}
.sheet(isPresented: $showSheet) {
FeedbackSheet()
.presentationDetents([.medium])
.presentationDetents([.large, .medium])
}
}
}
@@ -26,6 +27,9 @@ struct FeedbackSheet: View {
@State private var isSending = false
@State private var sent = false
@State private var error: String?
@State private var selectedPhoto: PhotosPickerItem?
@State private var photoData: Data?
@State private var photoPreview: UIImage?
var body: some View {
VStack(spacing: 16) {
@@ -64,6 +68,43 @@ struct FeedbackSheet: View {
.stroke(Color.textTertiary.opacity(0.2), lineWidth: 1)
)
// Photo attachment
HStack {
PhotosPicker(selection: $selectedPhoto, matching: .screenshots) {
HStack(spacing: 6) {
Image(systemName: "camera.fill")
.font(.caption)
Text(photoPreview != nil ? "Change photo" : "Add screenshot")
.font(.caption.weight(.medium))
}
.foregroundStyle(Color.accentWarm)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color.accentWarm.opacity(0.1))
.clipShape(Capsule())
}
if let preview = photoPreview {
Image(uiImage: preview)
.resizable()
.scaledToFill()
.frame(width: 40, height: 40)
.clipShape(RoundedRectangle(cornerRadius: 8))
Button {
photoData = nil
photoPreview = nil
selectedPhoto = nil
} label: {
Image(systemName: "xmark.circle.fill")
.font(.caption)
.foregroundStyle(Color.textTertiary)
}
}
Spacer()
}
if let error {
Text(error)
.font(.caption)
@@ -90,6 +131,15 @@ struct FeedbackSheet: View {
}
.padding()
.background(Color.surfaceCard)
.onChange(of: selectedPhoto) {
Task {
if let item = selectedPhoto,
let data = try? await item.loadTransferable(type: Data.self) {
photoData = data
photoPreview = UIImage(data: data)
}
}
}
}
private func sendFeedback() {
@@ -98,7 +148,10 @@ struct FeedbackSheet: View {
Task {
do {
let body: [String: String] = ["text": text.trimmingCharacters(in: .whitespaces), "source": "ios"]
var body: [String: Any] = ["text": text.trimmingCharacters(in: .whitespaces), "source": "ios"]
if let imgData = photoData {
body["image"] = imgData.base64EncodedString()
}
let jsonData = try JSONSerialization.data(withJSONObject: body)
let data = try await APIClient.shared.rawPost(path: "/api/feedback", data: jsonData)