Skip to content

Commit

Permalink
Merge branch 'release/2.4'
Browse files Browse the repository at this point in the history
  • Loading branch information
iHTCboy committed Apr 6, 2023
2 parents bf51ee1 + 2e2c213 commit 5ba2e08
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 61 deletions.
8 changes: 4 additions & 4 deletions iChatGPT.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2023.4.03;
CURRENT_PROJECT_VERSION = 2023.04.05;
DEVELOPMENT_ASSET_PATHS = "\"iChatGPT/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
Expand All @@ -587,7 +587,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.3;
MARKETING_VERSION = 2.4;
PRODUCT_BUNDLE_IDENTIFIER = com.37iOS.iChatGPT;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand All @@ -609,7 +609,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2023.4.03;
CURRENT_PROJECT_VERSION = 2023.04.05;
DEVELOPMENT_ASSET_PATHS = "\"iChatGPT/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
Expand All @@ -627,7 +627,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.3;
MARKETING_VERSION = 2.4;
PRODUCT_BUNDLE_IDENTIFIER = com.37iOS.iChatGPT;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down
203 changes: 148 additions & 55 deletions iChatGPT/AIChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,61 +13,15 @@ struct AIChatView: View {

@State private var isScrollListTop: Bool = false
@State private var isSettingsPresented: Bool = false
@State private var isSharing = false
@StateObject private var chatModel = AIChatModel(roomID: ChatRoomStore.shared.lastRoomId())
@StateObject private var inputModel = AIChatInputModel()
@StateObject private var shareContent = ShareContent()

var body: some View {
NavigationView {
VStack {
ScrollViewReader { proxy in
List {
ForEach(chatModel.contents, id: \.datetime) { item in
Section(header: Text(item.datetime)) {
VStack(alignment: .leading) {
HStack(alignment: .top) {
AvatarImageView(url: item.userAvatarUrl)
MarkdownText(item.issue.replacingOccurrences(of: "\n", with: "\n\n"))
.padding(.top, 3)
}
Divider()
HStack(alignment: .top) {
Image("chatgpt-icon")
.resizable()
.frame(width: 25, height: 25)
.cornerRadius(5)
.padding(.trailing, 10)
if item.isResponse {
MarkdownText(item.answer ?? "")
} else {
ProgressView()
Text("Loading..".localized())
.padding(.leading, 10)
}
}
.padding([.top, .bottom], 3)
}.contextMenu {
ChatContextMenu(searchText: $inputModel.searchText, chatModel: chatModel, item: item)
}
}
}
}
.listStyle(InsetGroupedListStyle())
.onChange(of: chatModel.isScrollListBottom) { _ in
if let lastId = chatModel.contents.last?.datetime {
withAnimation {
proxy.scrollTo(lastId, anchor: .trailing)
}
}
}
.onChange(of: isScrollListTop) { _ in
if let firstId = chatModel.contents.first?.datetime {
withAnimation {
proxy.scrollTo(firstId, anchor: .leading)
}
}
}
}

chatList
Spacer()
ChatInputView(searchText: $inputModel.searchText, chatModel: chatModel)
.padding([.leading, .trailing], 12)
Expand All @@ -79,12 +33,8 @@ struct AIChatView: View {
.markdownOrderedListBulletStyle(.custom)
.markdownUnorderedListBulletStyle(.custom)
.markdownImageStyle(.custom)
.navigationTitle("OpenAI ChatGPT")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
HStack {
addButton
})
.navigationBarItems(trailing: addButton)
.sheet(isPresented: $isSettingsPresented) {
ChatAPISettingView(isKeyPresented: $isSettingsPresented, chatModel: chatModel)
}
Expand All @@ -99,6 +49,9 @@ struct AIChatView: View {
.sheet(isPresented: $inputModel.isConfigChatRoom) {
ChatRoomConfigView(isKeyPresented: $inputModel.isConfigChatRoom)
}
.sheet(isPresented: $isSharing) {
ActivityView(activityItems: $shareContent.activityItems)
}
.alert(isPresented: $inputModel.showingAlert) {
switch inputModel.activeAlert {
case .createNewChatRoom:
Expand All @@ -107,6 +60,8 @@ struct AIChatView: View {
return ReloadLastQuestion()
case .clearAllQuestion:
return ClearAllQuestion()
case .shareContents:
return ShareContents()
}
}
.onChange(of: inputModel.isScrollToChatRoomTop) { _ in
Expand All @@ -126,6 +81,61 @@ struct AIChatView: View {
.environmentObject(inputModel)
}

@ViewBuilder
var chatList: some View {
ScrollViewReader { proxy in
List {
ForEach(chatModel.contents, id: \.datetime) { item in
Section(header: Text(item.datetime)) {
VStack(alignment: .leading) {
HStack(alignment: .top) {
AvatarImageView(url: item.userAvatarUrl)
MarkdownText(item.issue.replacingOccurrences(of: "\n", with: "\n\n"))
.padding(.top, 3)
}
Divider()
HStack(alignment: .top) {
Image("chatgpt-icon")
.resizable()
.frame(width: 25, height: 25)
.cornerRadius(5)
.padding(.trailing, 10)
if item.isResponse {
MarkdownText(item.answer ?? "")
} else {
ProgressView()
Text("Loading..".localized())
.padding(.leading, 10)
}
}
.padding([.top, .bottom], 3)
}.contextMenu {
ChatContextMenu(searchText: $inputModel.searchText, chatModel: chatModel, item: item)
}
}
}
}
.listStyle(InsetGroupedListStyle())
.onChange(of: chatModel.isScrollListBottom) { _ in
if let lastId = chatModel.contents.last?.datetime {
// try fix macOS crash
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
withAnimation {
proxy.scrollTo(lastId, anchor: .trailing)
}
}
}
}
.onChange(of: isScrollListTop) { _ in
if let firstId = chatModel.contents.first?.datetime {
withAnimation {
proxy.scrollTo(firstId, anchor: .leading)
}
}
}
}
}

private var addButton: some View {
Button(action: {
isSettingsPresented.toggle()
Expand All @@ -136,7 +146,9 @@ struct AIChatView: View {
} else {
Image(systemName: "key.icloud").imageScale(.large)
}
}.frame(height: 40)
}
.frame(height: 40)
.padding(.trailing, 5)
}
}
}
Expand Down Expand Up @@ -178,6 +190,87 @@ extension AIChatView {
secondaryButton: .cancel()
)
}

func ShareContents() -> Alert {
Alert(title: Text("Share"),
message: Text("Choose a sharing format"),
primaryButton: .default(Text("Image")) {
screenshotAndShare(isImage: true)
},
secondaryButton: .default(Text("PDF")) {
screenshotAndShare(isImage: false)
}
)
}
}



// MARK: - Handle Share Image/PDF
extension AIChatView {

private func screenshotAndShare(isImage: Bool) {
if let image = screenshot() {
if isImage {
shareContent.activityItems = [image]
isSharing = true
} else {
if let pdfData = imageToPDFData(image: image) {
let temporaryDirectoryURL = FileManager.default.temporaryDirectory
let fileName = "iChatGPT-Screenshot.pdf"
let fileURL = temporaryDirectoryURL.appendingPathComponent(fileName)

do {
try pdfData.write(to: fileURL, options: .atomic)
shareContent.activityItems = [fileURL]
isSharing = true
} catch {
print("Error writing PDF data to file: \(error)")
}
}
}
}
}

private func screenshot() -> UIImage? {
let controller = UIHostingController(rootView: self)
let view = controller.view

let targetSize = UIScreen.main.bounds.size
view?.frame = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear

let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}

private func imageToPDFData(image: UIImage) -> Data? {
let pdfRenderer = UIGraphicsPDFRenderer(bounds: CGRect(origin: .zero, size: image.size))
let pdfData = pdfRenderer.pdfData { (context) in
context.beginPage()
image.draw(in: CGRect(origin: .zero, size: image.size))
}
return pdfData
}
}

class ShareContent: ObservableObject {
@Published var activityItems: [Any] = []
}

// MARK: Render UIActivityViewController
struct ActivityView: UIViewControllerRepresentable {
@Binding var activityItems: [Any]

func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
return controller
}

func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityView>) {
}
}

// MARK: Avatar Image View
Expand Down
4 changes: 3 additions & 1 deletion iChatGPT/ChatHistoryListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ struct ChatHistoryListView: View {
Text(" \(ChatMessageStore.shared.messages(forRoom: item.roomID).count) ")
.font(.footnote)
.foregroundColor(.white)
.padding(5)
.padding([.top, .bottom], 3)
.padding([.leading, .trailing], 4)
.background(Color.blue.opacity(0.8))
.clipShape(Capsule())
}
Expand All @@ -101,6 +102,7 @@ struct ChatHistoryListView: View {
Text(ChatMessageStore.shared.lastMessage(item.roomID)?.issue ?? "No conversations".localized())
.font(.subheadline)
.foregroundColor(.gray)
.lineLimit(2)

Spacer()

Expand Down
10 changes: 10 additions & 0 deletions iChatGPT/ChatInputView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ struct ChatInputView: View {
.padding(.trailing, 5)
.foregroundColor(.lightGray)
.buttonStyle(PlainButtonStyle())

Button(action: {
model.activeAlert = .shareContents
model.showingAlert.toggle()
}) {
Image(systemName: "square.and.arrow.up")
}
.padding(.trailing, 5)
.foregroundColor(.lightGray)
.buttonStyle(PlainButtonStyle())
}

Button(action: {
Expand Down
2 changes: 1 addition & 1 deletion iChatGPT/Models/AIChatInputModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import Foundation

enum InputViewAlert {
case createNewChatRoom, reloadLastQuestion, clearAllQuestion
case createNewChatRoom, reloadLastQuestion, clearAllQuestion, shareContents
}

class AIChatInputModel: ObservableObject {
Expand Down

0 comments on commit 5ba2e08

Please sign in to comment.