Skip to content

Commit 5ba2e08

Browse files
committed
Merge branch 'release/2.4'
2 parents bf51ee1 + 2e2c213 commit 5ba2e08

File tree

5 files changed

+166
-61
lines changed

5 files changed

+166
-61
lines changed

iChatGPT.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@
569569
CODE_SIGN_IDENTITY = "Apple Development";
570570
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
571571
CODE_SIGN_STYLE = Manual;
572-
CURRENT_PROJECT_VERSION = 2023.4.03;
572+
CURRENT_PROJECT_VERSION = 2023.04.05;
573573
DEVELOPMENT_ASSET_PATHS = "\"iChatGPT/Preview Content\"";
574574
DEVELOPMENT_TEAM = "";
575575
ENABLE_PREVIEWS = YES;
@@ -587,7 +587,7 @@
587587
"$(inherited)",
588588
"@executable_path/Frameworks",
589589
);
590-
MARKETING_VERSION = 2.3;
590+
MARKETING_VERSION = 2.4;
591591
PRODUCT_BUNDLE_IDENTIFIER = com.37iOS.iChatGPT;
592592
PRODUCT_NAME = "$(TARGET_NAME)";
593593
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -609,7 +609,7 @@
609609
CODE_SIGN_IDENTITY = "Apple Development";
610610
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
611611
CODE_SIGN_STYLE = Manual;
612-
CURRENT_PROJECT_VERSION = 2023.4.03;
612+
CURRENT_PROJECT_VERSION = 2023.04.05;
613613
DEVELOPMENT_ASSET_PATHS = "\"iChatGPT/Preview Content\"";
614614
DEVELOPMENT_TEAM = "";
615615
ENABLE_PREVIEWS = YES;
@@ -627,7 +627,7 @@
627627
"$(inherited)",
628628
"@executable_path/Frameworks",
629629
);
630-
MARKETING_VERSION = 2.3;
630+
MARKETING_VERSION = 2.4;
631631
PRODUCT_BUNDLE_IDENTIFIER = com.37iOS.iChatGPT;
632632
PRODUCT_NAME = "$(TARGET_NAME)";
633633
PROVISIONING_PROFILE_SPECIFIER = "";

iChatGPT/AIChatView.swift

Lines changed: 148 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -13,61 +13,15 @@ struct AIChatView: View {
1313

1414
@State private var isScrollListTop: Bool = false
1515
@State private var isSettingsPresented: Bool = false
16+
@State private var isSharing = false
1617
@StateObject private var chatModel = AIChatModel(roomID: ChatRoomStore.shared.lastRoomId())
1718
@StateObject private var inputModel = AIChatInputModel()
19+
@StateObject private var shareContent = ShareContent()
1820

1921
var body: some View {
2022
NavigationView {
2123
VStack {
22-
ScrollViewReader { proxy in
23-
List {
24-
ForEach(chatModel.contents, id: \.datetime) { item in
25-
Section(header: Text(item.datetime)) {
26-
VStack(alignment: .leading) {
27-
HStack(alignment: .top) {
28-
AvatarImageView(url: item.userAvatarUrl)
29-
MarkdownText(item.issue.replacingOccurrences(of: "\n", with: "\n\n"))
30-
.padding(.top, 3)
31-
}
32-
Divider()
33-
HStack(alignment: .top) {
34-
Image("chatgpt-icon")
35-
.resizable()
36-
.frame(width: 25, height: 25)
37-
.cornerRadius(5)
38-
.padding(.trailing, 10)
39-
if item.isResponse {
40-
MarkdownText(item.answer ?? "")
41-
} else {
42-
ProgressView()
43-
Text("Loading..".localized())
44-
.padding(.leading, 10)
45-
}
46-
}
47-
.padding([.top, .bottom], 3)
48-
}.contextMenu {
49-
ChatContextMenu(searchText: $inputModel.searchText, chatModel: chatModel, item: item)
50-
}
51-
}
52-
}
53-
}
54-
.listStyle(InsetGroupedListStyle())
55-
.onChange(of: chatModel.isScrollListBottom) { _ in
56-
if let lastId = chatModel.contents.last?.datetime {
57-
withAnimation {
58-
proxy.scrollTo(lastId, anchor: .trailing)
59-
}
60-
}
61-
}
62-
.onChange(of: isScrollListTop) { _ in
63-
if let firstId = chatModel.contents.first?.datetime {
64-
withAnimation {
65-
proxy.scrollTo(firstId, anchor: .leading)
66-
}
67-
}
68-
}
69-
}
70-
24+
chatList
7125
Spacer()
7226
ChatInputView(searchText: $inputModel.searchText, chatModel: chatModel)
7327
.padding([.leading, .trailing], 12)
@@ -79,12 +33,8 @@ struct AIChatView: View {
7933
.markdownOrderedListBulletStyle(.custom)
8034
.markdownUnorderedListBulletStyle(.custom)
8135
.markdownImageStyle(.custom)
82-
.navigationTitle("OpenAI ChatGPT")
8336
.navigationBarTitleDisplayMode(.inline)
84-
.navigationBarItems(trailing:
85-
HStack {
86-
addButton
87-
})
37+
.navigationBarItems(trailing: addButton)
8838
.sheet(isPresented: $isSettingsPresented) {
8939
ChatAPISettingView(isKeyPresented: $isSettingsPresented, chatModel: chatModel)
9040
}
@@ -99,6 +49,9 @@ struct AIChatView: View {
9949
.sheet(isPresented: $inputModel.isConfigChatRoom) {
10050
ChatRoomConfigView(isKeyPresented: $inputModel.isConfigChatRoom)
10151
}
52+
.sheet(isPresented: $isSharing) {
53+
ActivityView(activityItems: $shareContent.activityItems)
54+
}
10255
.alert(isPresented: $inputModel.showingAlert) {
10356
switch inputModel.activeAlert {
10457
case .createNewChatRoom:
@@ -107,6 +60,8 @@ struct AIChatView: View {
10760
return ReloadLastQuestion()
10861
case .clearAllQuestion:
10962
return ClearAllQuestion()
63+
case .shareContents:
64+
return ShareContents()
11065
}
11166
}
11267
.onChange(of: inputModel.isScrollToChatRoomTop) { _ in
@@ -126,6 +81,61 @@ struct AIChatView: View {
12681
.environmentObject(inputModel)
12782
}
12883

84+
@ViewBuilder
85+
var chatList: some View {
86+
ScrollViewReader { proxy in
87+
List {
88+
ForEach(chatModel.contents, id: \.datetime) { item in
89+
Section(header: Text(item.datetime)) {
90+
VStack(alignment: .leading) {
91+
HStack(alignment: .top) {
92+
AvatarImageView(url: item.userAvatarUrl)
93+
MarkdownText(item.issue.replacingOccurrences(of: "\n", with: "\n\n"))
94+
.padding(.top, 3)
95+
}
96+
Divider()
97+
HStack(alignment: .top) {
98+
Image("chatgpt-icon")
99+
.resizable()
100+
.frame(width: 25, height: 25)
101+
.cornerRadius(5)
102+
.padding(.trailing, 10)
103+
if item.isResponse {
104+
MarkdownText(item.answer ?? "")
105+
} else {
106+
ProgressView()
107+
Text("Loading..".localized())
108+
.padding(.leading, 10)
109+
}
110+
}
111+
.padding([.top, .bottom], 3)
112+
}.contextMenu {
113+
ChatContextMenu(searchText: $inputModel.searchText, chatModel: chatModel, item: item)
114+
}
115+
}
116+
}
117+
}
118+
.listStyle(InsetGroupedListStyle())
119+
.onChange(of: chatModel.isScrollListBottom) { _ in
120+
if let lastId = chatModel.contents.last?.datetime {
121+
// try fix macOS crash
122+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
123+
withAnimation {
124+
proxy.scrollTo(lastId, anchor: .trailing)
125+
}
126+
}
127+
}
128+
}
129+
.onChange(of: isScrollListTop) { _ in
130+
if let firstId = chatModel.contents.first?.datetime {
131+
withAnimation {
132+
proxy.scrollTo(firstId, anchor: .leading)
133+
}
134+
}
135+
}
136+
}
137+
}
138+
129139
private var addButton: some View {
130140
Button(action: {
131141
isSettingsPresented.toggle()
@@ -136,7 +146,9 @@ struct AIChatView: View {
136146
} else {
137147
Image(systemName: "key.icloud").imageScale(.large)
138148
}
139-
}.frame(height: 40)
149+
}
150+
.frame(height: 40)
151+
.padding(.trailing, 5)
140152
}
141153
}
142154
}
@@ -178,6 +190,87 @@ extension AIChatView {
178190
secondaryButton: .cancel()
179191
)
180192
}
193+
194+
func ShareContents() -> Alert {
195+
Alert(title: Text("Share"),
196+
message: Text("Choose a sharing format"),
197+
primaryButton: .default(Text("Image")) {
198+
screenshotAndShare(isImage: true)
199+
},
200+
secondaryButton: .default(Text("PDF")) {
201+
screenshotAndShare(isImage: false)
202+
}
203+
)
204+
}
205+
}
206+
207+
208+
209+
// MARK: - Handle Share Image/PDF
210+
extension AIChatView {
211+
212+
private func screenshotAndShare(isImage: Bool) {
213+
if let image = screenshot() {
214+
if isImage {
215+
shareContent.activityItems = [image]
216+
isSharing = true
217+
} else {
218+
if let pdfData = imageToPDFData(image: image) {
219+
let temporaryDirectoryURL = FileManager.default.temporaryDirectory
220+
let fileName = "iChatGPT-Screenshot.pdf"
221+
let fileURL = temporaryDirectoryURL.appendingPathComponent(fileName)
222+
223+
do {
224+
try pdfData.write(to: fileURL, options: .atomic)
225+
shareContent.activityItems = [fileURL]
226+
isSharing = true
227+
} catch {
228+
print("Error writing PDF data to file: \(error)")
229+
}
230+
}
231+
}
232+
}
233+
}
234+
235+
private func screenshot() -> UIImage? {
236+
let controller = UIHostingController(rootView: self)
237+
let view = controller.view
238+
239+
let targetSize = UIScreen.main.bounds.size
240+
view?.frame = CGRect(origin: .zero, size: targetSize)
241+
view?.backgroundColor = .clear
242+
243+
let renderer = UIGraphicsImageRenderer(size: targetSize)
244+
return renderer.image { _ in
245+
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
246+
}
247+
}
248+
249+
private func imageToPDFData(image: UIImage) -> Data? {
250+
let pdfRenderer = UIGraphicsPDFRenderer(bounds: CGRect(origin: .zero, size: image.size))
251+
let pdfData = pdfRenderer.pdfData { (context) in
252+
context.beginPage()
253+
image.draw(in: CGRect(origin: .zero, size: image.size))
254+
}
255+
return pdfData
256+
}
257+
}
258+
259+
class ShareContent: ObservableObject {
260+
@Published var activityItems: [Any] = []
261+
}
262+
263+
// MARK: Render UIActivityViewController
264+
struct ActivityView: UIViewControllerRepresentable {
265+
@Binding var activityItems: [Any]
266+
267+
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
268+
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
269+
return controller
270+
}
271+
272+
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityView>) {
273+
}
181274
}
182275

183276
// MARK: Avatar Image View

iChatGPT/ChatHistoryListView.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ struct ChatHistoryListView: View {
9191
Text(" \(ChatMessageStore.shared.messages(forRoom: item.roomID).count) ")
9292
.font(.footnote)
9393
.foregroundColor(.white)
94-
.padding(5)
94+
.padding([.top, .bottom], 3)
95+
.padding([.leading, .trailing], 4)
9596
.background(Color.blue.opacity(0.8))
9697
.clipShape(Capsule())
9798
}
@@ -101,6 +102,7 @@ struct ChatHistoryListView: View {
101102
Text(ChatMessageStore.shared.lastMessage(item.roomID)?.issue ?? "No conversations".localized())
102103
.font(.subheadline)
103104
.foregroundColor(.gray)
105+
.lineLimit(2)
104106

105107
Spacer()
106108

iChatGPT/ChatInputView.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ struct ChatInputView: View {
5252
.padding(.trailing, 5)
5353
.foregroundColor(.lightGray)
5454
.buttonStyle(PlainButtonStyle())
55+
56+
Button(action: {
57+
model.activeAlert = .shareContents
58+
model.showingAlert.toggle()
59+
}) {
60+
Image(systemName: "square.and.arrow.up")
61+
}
62+
.padding(.trailing, 5)
63+
.foregroundColor(.lightGray)
64+
.buttonStyle(PlainButtonStyle())
5565
}
5666

5767
Button(action: {

iChatGPT/Models/AIChatInputModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import Foundation
1010

1111
enum InputViewAlert {
12-
case createNewChatRoom, reloadLastQuestion, clearAllQuestion
12+
case createNewChatRoom, reloadLastQuestion, clearAllQuestion, shareContents
1313
}
1414

1515
class AIChatInputModel: ObservableObject {

0 commit comments

Comments
 (0)