11import Cocoa
2- import Foundation
32import SwiftSignalKit
43import TelegramCore
54import TGUIKit
@@ -21,20 +20,165 @@ private final class Arguments {
2120 public let context : any AccountContextProtocol
2221 public let toggleChats : ( Bool ) -> Void
2322 public let toggleIncomingCalls : ( Bool ) -> Void
23+ public let openLocation : ( String ) -> Void
2424 public let rename : ( NSView ) -> Void
25- public init ( context : any AccountContextProtocol , toggleChats : @escaping ( Bool ) -> Void , toggleIncomingCalls : @escaping ( Bool ) -> Void , rename : @escaping ( NSView ) -> Void ) {
25+ public init ( context : any AccountContextProtocol , toggleChats : @escaping ( Bool ) -> Void , toggleIncomingCalls : @escaping ( Bool ) -> Void , openLocation : @escaping ( String ) -> Void , rename : @escaping ( NSView ) -> Void ) {
2626 self . context = context
2727 self . toggleChats = toggleChats
2828 self . toggleIncomingCalls = toggleIncomingCalls
29+ self . openLocation = openLocation
2930 self . rename = rename
3031 }
3132}
3233
3334private struct State : Equatable {
3435 public let session : RecentAccountSession
36+ public let isGeocoding : Bool
3537 public let deviceModel : String ?
3638}
3739
40+ private final class SessionDetailRowItem : GeneralInteractedRowItem {
41+ let valueLayout : TextViewLayout
42+ let accessoryImage : CGImage ?
43+ let accessoryTooltip : String ?
44+ let accessoryAction : ( ( ) -> Void ) ?
45+ let accessoryLoading : Bool
46+
47+ var accessoryInset : CGFloat {
48+ return accessoryImage . map { $0 . systemSize . width + 10 } ?? 0
49+ }
50+
51+ var valueWidth : CGFloat {
52+ return max ( 1 , blockWidth - nameLayout . layoutSize . width - viewType . innerInset . right - inset . left - 10 - accessoryInset )
53+ }
54+
55+ init ( _ initialSize : NSSize , stableId : AnyHashable , name : String , value : String , viewType : GeneralViewType , accessoryImage : CGImage ? , accessoryTooltip : String ? , accessoryAction : ( ( ) -> Void ) ? , accessoryLoading : Bool ) {
56+ self . accessoryImage = accessoryImage
57+ self . accessoryTooltip = accessoryTooltip
58+ self . accessoryAction = accessoryAction
59+ self . accessoryLoading = accessoryLoading
60+ valueLayout = TextViewLayout ( . initialize ( string : value , color : theme . colors . grayText , font : . normal ( . title ) ) , alignment : . right )
61+ let type : GeneralInteractedType
62+ if let accessoryImage , accessoryAction == nil {
63+ type = . imageContext ( accessoryImage , value )
64+ } else {
65+ type = . context ( value )
66+ }
67+ super . init ( initialSize , stableId : stableId , name : name , type : type , viewType : viewType )
68+ }
69+
70+ override func makeSize ( _ width : CGFloat , oldWidth : CGFloat ) -> Bool {
71+ let result = super . makeSize ( width , oldWidth : oldWidth )
72+ valueLayout . measure ( width : valueWidth )
73+ return result
74+ }
75+
76+ override var height : CGFloat {
77+ return max ( super . height , valueLayout . layoutSize . height + viewType . innerInset . top + viewType . innerInset . bottom )
78+ }
79+
80+ override func viewClass ( ) -> AnyClass {
81+ return SessionDetailRowView . self
82+ }
83+
84+ override var accessibilityHelp : String ? {
85+ return accessoryLoading ? strings ( ) . voiceOverCommonLoading : accessoryTooltip
86+ }
87+
88+ override var accessibilityRole : NSAccessibility . Role ? {
89+ return accessoryAction == nil ? super . accessibilityRole : . group
90+ }
91+
92+ override var accessibilityIncludesChildren : Bool {
93+ return accessoryAction != nil
94+ }
95+ }
96+
97+ private final class SessionDetailRowView : GeneralInteractedRowView {
98+ private var accessoryControl : Control ?
99+ private var accessoryProgress : ProgressIndicator ?
100+
101+ override func set ( item : TableRowItem , animated : Bool = false ) {
102+ super . set ( item : item , animated : animated )
103+ guard let item = item as? SessionDetailRowItem else {
104+ return
105+ }
106+ if let image = item . accessoryImage {
107+ let control : Control
108+ if let current = accessoryControl {
109+ control = current
110+ } else {
111+ control = item . accessoryAction == nil ? OverlayControl ( ) : ImageButton ( )
112+ control . set ( handler : { control in
113+ if control . controlState != . Hover {
114+ dismissTooltip ( for : control )
115+ }
116+ } , for : . Other )
117+ if item . accessoryAction != nil {
118+ control . set ( handler : { [ weak self ] _ in
119+ ( self ? . item as? SessionDetailRowItem ) ? . accessoryAction ? ( )
120+ } , for : . Click )
121+ }
122+ containerView . addSubview ( control )
123+ accessoryControl = control
124+ }
125+ ( control as? ImageButton ) ? . set ( image : image , for : . Normal )
126+ control . setFrameSize ( NSMakeSize ( image . systemSize . width + 8 , image . systemSize . height + 8 ) )
127+ control . appTooltip = item . accessoryLoading ? nil : item . accessoryTooltip
128+ control . setAccessibilityLabel ( item . accessoryTooltip )
129+ control . isAccessibilityDecorative = item . accessoryAction == nil
130+ control . isEnabled = ! item . accessoryLoading
131+ control . isHidden = item . accessoryLoading
132+ if item . accessoryLoading {
133+ dismissTooltip ( for : control )
134+ }
135+ } else {
136+ if let accessoryControl {
137+ dismissTooltip ( for : accessoryControl )
138+ }
139+ accessoryControl ? . removeFromSuperview ( )
140+ accessoryControl = nil
141+ }
142+ if item . accessoryLoading {
143+ let progress : ProgressIndicator
144+ if let current = accessoryProgress {
145+ progress = current
146+ } else {
147+ progress = ProgressIndicator ( frame : NSMakeRect ( 0 , 0 , 20 , 20 ) )
148+ progress . userInteractionEnabled = false
149+ progress . isAccessibilityDecorative = true
150+ containerView . addSubview ( progress )
151+ accessoryProgress = progress
152+ }
153+ progress . progressColor = theme . colors . accent
154+ } else {
155+ accessoryProgress ? . removeFromSuperview ( )
156+ accessoryProgress = nil
157+ }
158+ }
159+
160+ override func layout ( ) {
161+ super . layout ( )
162+ guard let item = item as? SessionDetailRowItem , let textView = textView else {
163+ return
164+ }
165+ item . valueLayout . measure ( width : item . valueWidth )
166+ textView . update ( item . valueLayout )
167+ textView . centerY ( x : containerView . frame . width - item . viewType . innerInset . right - textView . frame . width - item . accessoryInset )
168+ if let accessoryControl , let image = item . accessoryImage {
169+ accessoryControl . centerY ( x : containerView . frame . width - item . viewType . innerInset . right - image . systemSize . width - 4 , addition : - 1 )
170+ accessoryProgress ? . centerY ( x : accessoryControl . frame . midX - 10 , addition : - 1 )
171+ }
172+ }
173+ }
174+
175+ private func sessionDetailEntry ( sectionId : Int32 , index : Int32 , identifier : InputDataIdentifier , name : String , value : String , viewType : GeneralViewType , accessoryImage : CGImage ? = nil , accessoryTooltip : String ? = nil , accessoryAction : ( ( ) -> Void ) ? = nil , accessoryLoading : Bool = false ) -> InputDataEntry {
176+ let type : GeneralInteractedType = accessoryLoading ? . loading : accessoryImage . map { . imageContext ( $0 , value ) } ?? . context ( value )
177+ let data = InputDataGeneralData ( name : name , color : theme . colors . text , type : type , viewType : viewType , description : accessoryTooltip )
178+ return . custom ( sectionId : sectionId , index : index , value : . none , identifier : identifier , equatable : InputDataEquatable ( data ) , comparable : nil , item : { initialSize , stableId in
179+ return SessionDetailRowItem ( initialSize , stableId : stableId , name : name , value : value , viewType : viewType , accessoryImage : accessoryImage , accessoryTooltip : accessoryTooltip , accessoryAction : accessoryAction , accessoryLoading : accessoryLoading )
180+ } )
181+ }
38182
39183private func entries ( _ state : State , arguments : Arguments ) -> [ InputDataEntry ] {
40184 var entries : [ InputDataEntry ] = [ ]
@@ -69,22 +213,57 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] {
69213
70214 entries . append ( . sectionId ( sectionId , type : . normal ) )
71215 sectionId += 1
72-
73-
74-
75- entries . append ( . general ( sectionId : sectionId , index : index , value : . none , error : nil , identifier : . init ( " application " ) , data : InputDataGeneralData ( name : strings ( ) . sessionPreviewApp , color : theme . colors . text , type : . context ( state . session . appName + " , " + state . session . appVersion ) , viewType : state . session . ip . isEmpty ? . singleItem : . firstItem , enabled : true ) ) )
216+ entries . append ( . desc ( sectionId : sectionId , index : index , text : . plain ( " Device " ) , data : . init ( color : theme . colors . listGrayText , viewType : . textTopItem ) ) ) //TODOLANG
76217 index += 1
77-
78- if ! state . session . ip . isEmpty {
79- entries . append ( . general ( sectionId : sectionId , index : index , value : . none , error : nil , identifier : . init ( " ip " ) , data : InputDataGeneralData ( name : strings ( ) . sessionPreviewIp , color : theme . colors . text , type : . context ( state . session . ip ) , viewType : . innerItem , enabled : true ) ) )
218+
219+ let isOfficial = state . session . flags . contains ( . isOfficial )
220+ entries . append ( sessionDetailEntry ( sectionId : sectionId , index : index , identifier : . init ( " application " ) , name : strings ( ) . sessionPreviewApp , value : state . session . appName , viewType : . firstItem , accessoryImage : isOfficial ? theme . icons . verifyDialog : nil , accessoryTooltip : isOfficial ? " This session is from the official Telegram app " : nil ) ) //TODOLANG
221+ index += 1
222+
223+ entries . append ( . general ( sectionId : sectionId , index : index , value : . none , error : nil , identifier : . init ( " version " ) , data : InputDataGeneralData ( name : " Version " , color : theme . colors . text , type : . context ( state . session . appVersion ) , viewType : . innerItem , enabled : true ) ) ) //TODOLANG
224+ index += 1
225+
226+ let system = [ state . session . platform , state . session . systemVersion ] . map { $0 . trimmingCharacters ( in : . whitespacesAndNewlines ) } . filter { ! $0 . isEmpty } . joined ( separator : " " )
227+ entries . append ( . general ( sectionId : sectionId , index : index , value : . none , error : nil , identifier : . init ( " os " ) , data : InputDataGeneralData ( name : " OS " , color : theme . colors . text , type : . context ( system ) , viewType : . lastItem , enabled : true ) ) ) //TODOLANG
228+ index += 1
229+
230+ entries . append ( . sectionId ( sectionId , type : . normal ) )
231+ sectionId += 1
232+
233+ entries . append ( . desc ( sectionId : sectionId , index : index , text : . plain ( " Connection " ) , data : . init ( color : theme . colors . listGrayText , viewType : . textTopItem ) ) ) //TODOLANG
234+ index += 1
235+
236+ let ip = state . session . ip . trimmingCharacters ( in : . whitespacesAndNewlines )
237+ let location = [ state . session . country , state . session . region ] . map { $0 . trimmingCharacters ( in : . whitespacesAndNewlines ) } . filter { ! $0 . isEmpty } . joined ( separator : " " )
238+
239+ if ! ip . isEmpty {
240+ entries . append ( sessionDetailEntry ( sectionId : sectionId , index : index , identifier : . init ( " ip " ) , name : strings ( ) . sessionPreviewIp , value : ip , viewType : . firstItem ) )
80241 index += 1
81-
82- entries . append ( . general ( sectionId : sectionId , index : index , value : . none , error : nil , identifier : . init ( " location " ) , data : InputDataGeneralData ( name : strings ( ) . sessionPreviewLocation , color : theme . colors . text , type : . context ( state . session . country ) , viewType : . lastItem , enabled : true ) ) )
242+ }
243+
244+ if ! location . isEmpty {
245+ entries . append ( sessionDetailEntry ( sectionId : sectionId , index : index , identifier : . init ( " location " ) , name : strings ( ) . sessionPreviewLocation , value : location , viewType : ip . isEmpty ? . firstItem : . innerItem , accessoryImage : theme . icons . chatAttachLocation , accessoryTooltip : strings ( ) . locationPreviewTitle , accessoryAction : {
246+ arguments . openLocation ( location )
247+ } , accessoryLoading : state . isGeocoding ) )
83248 index += 1
84-
249+ }
250+
251+ let dateFormatter = DateFormatter ( )
252+ dateFormatter . locale = appAppearance . locale
253+ dateFormatter . timeZone = . current
254+ dateFormatter . dateStyle = . medium
255+ dateFormatter . timeStyle = . medium
256+ dateFormatter . doesRelativeDateFormatting = false
257+
258+ entries . append ( sessionDetailEntry ( sectionId : sectionId , index : index , identifier : . init ( " first_signed_at " ) , name : " First Signed At " , value : dateFormatter . string ( from : Date ( timeIntervalSince1970 : TimeInterval ( state . session . creationDate ) ) ) , viewType : ip . isEmpty && location . isEmpty ? . firstItem : . innerItem ) ) //TODOLANG
259+ index += 1
260+
261+ entries . append ( sessionDetailEntry ( sectionId : sectionId , index : index , identifier : . init ( " last_active " ) , name : " Last Active " , value : dateFormatter . string ( from : Date ( timeIntervalSince1970 : TimeInterval ( state . session . activityDate ) ) ) , viewType : . lastItem ) ) //TODOLANG
262+ index += 1
263+
264+ if ! location . isEmpty {
85265 entries . append ( . desc ( sectionId : sectionId , index : index , text : . plain ( strings ( ) . sessionPreviewIpDesc ) , data : . init ( color : theme . colors . listGrayText , viewType : . textBottomItem ) ) )
86266 index += 1
87-
88267 }
89268
90269
@@ -119,6 +298,9 @@ public func SessionModalController(context: any AccountContextProtocol, session:
119298
120299 var close : ( ( ) -> Void ) ? = nil
121300 var isUpdating = false
301+ let locationDisposable = MetaDisposable ( )
302+ let geocodingPromise = ValuePromise ( false , ignoreRepeated : true )
303+ var isGeocoding = false
122304 var deviceModelUpdates = 0
123305 let deviceModelDisposable = renamedDeviceModel . get ( ) . start ( next : { _ in
124306 deviceModelUpdates += 1
@@ -153,15 +335,36 @@ public func SessionModalController(context: any AccountContextProtocol, session:
153335 updateSession ( context . activeSessionsContext . updateSessionAcceptsSecretChats ( session , accepts : updated ) )
154336 } , toggleIncomingCalls : { updated in
155337 updateSession ( context . activeSessionsContext . updateSessionAcceptsIncomingCalls ( session , accepts : updated ) )
338+ } , openLocation : { location in
339+ guard ! isGeocoding else {
340+ return
341+ }
342+ isGeocoding = true
343+ geocodingPromise . set ( true )
344+ let signal = FeatureNavigationHooks . shared . makeLocationPreviewController ( context , location )
345+ |> deliverOnMainQueue
346+ |> afterDisposed {
347+ Queue . mainQueue ( ) . async {
348+ isGeocoding = false
349+ geocodingPromise . set ( false )
350+ }
351+ }
352+ locationDisposable . set ( signal . start ( next : { controller in
353+ if let controller {
354+ showModal ( with : controller , for : context . window )
355+ } else {
356+ alert ( for : context . window , info : " Couldn't find this location. Please try again. " ) //TODOLANG
357+ }
358+ } ) )
156359 } , rename : { source in
157360 showModal ( with : RenameDeviceController ( context : context ) , for : context . window , morphSource : { [ weak source ] in
158361 return source
159362 } )
160363 } )
161364
162- let signal = combineLatest ( context . activeSessionsContext . state , renamedDeviceModel . get ( ) )
163- |> map { state , deviceModel in
164- return State ( session : state . sessions . first ( where : { $0 . hash == session . hash } ) ?? session , deviceModel : session . isCurrent ? deviceModel : nil )
365+ let signal = combineLatest ( context . activeSessionsContext . state , geocodingPromise . get ( ) , renamedDeviceModel . get ( ) )
366+ |> map { state , isGeocoding , deviceModel in
367+ return State ( session : state . sessions . first ( where : { $0 . hash == session . hash } ) ?? session , isGeocoding : isGeocoding , deviceModel : session . isCurrent ? deviceModel : nil )
165368 }
166369 |> distinctUntilChanged
167370 |> deliverOnPrepareQueue
@@ -170,6 +373,10 @@ public func SessionModalController(context: any AccountContextProtocol, session:
170373 }
171374
172375 let controller = InputDataController ( dataSignal : signal , title : strings ( ) . sessionPreviewTitle )
376+ controller . onDeinit = {
377+ locationDisposable . dispose ( )
378+ deviceModelDisposable . dispose ( )
379+ }
173380
174381 let modalInteractions : ModalInteractions
175382 if session . isCurrent {
@@ -205,13 +412,9 @@ public func SessionModalController(context: any AccountContextProtocol, session:
205412 }
206413 }
207414
208- controller . onDeinit = {
209- deviceModelDisposable . dispose ( )
210- }
211-
212415 let modalController = InputDataModalController ( controller , modalInteractions : modalInteractions , closeHandler : { _ in
213416 close ? ( )
214- } )
417+ } , size : NSMakeSize ( 440 , 300 ) )
215418
216419 controller . leftModalHeader = ModalHeaderData ( image : theme . icons . modalClose , handler : { [ weak modalController ] in
217420 modalController ? . close ( )
0 commit comments