@@ -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
0 commit comments