mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			228 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			228 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Swift
		
	
| // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | |
| 
 | |
| import Foundation
 | |
| import GRDB
 | |
| import SessionUtilitiesKit
 | |
| import SessionSnodeKit
 | |
| 
 | |
| public class TypingIndicators {
 | |
|     // MARK: - Direction
 | |
|     
 | |
|     public enum Direction {
 | |
|         case outgoing
 | |
|         case incoming
 | |
|     }
 | |
|     
 | |
|     private class Indicator {
 | |
|         fileprivate let threadId: String
 | |
|         fileprivate let threadVariant: SessionThread.Variant
 | |
|         fileprivate let direction: Direction
 | |
|         fileprivate let timestampMs: Int64
 | |
|         
 | |
|         fileprivate var refreshTimer: Timer?
 | |
|         fileprivate var stopTimer: Timer?
 | |
|         
 | |
|         init?(
 | |
|             threadId: String,
 | |
|             threadVariant: SessionThread.Variant,
 | |
|             threadIsBlocked: Bool,
 | |
|             threadIsMessageRequest: Bool,
 | |
|             direction: Direction,
 | |
|             timestampMs: Int64?
 | |
|         ) {
 | |
|             // The `typingIndicatorsEnabled` flag reflects the user-facing setting in the app
 | |
|             // preferences, if it's disabled we don't want to emit "typing indicator" messages
 | |
|             // or show typing indicators for other users
 | |
|             //
 | |
|             // We also don't want to show/send typing indicators for message requests
 | |
|             guard
 | |
|                 Storage.shared[.typingIndicatorsEnabled] &&
 | |
|                 !threadIsBlocked &&
 | |
|                 !threadIsMessageRequest
 | |
|             else { return nil }
 | |
|             
 | |
|             // Don't send typing indicators in group threads
 | |
|             guard
 | |
|                 threadVariant != .legacyGroup &&
 | |
|                 threadVariant != .group &&
 | |
|                 threadVariant != .community
 | |
|             else { return nil }
 | |
|             
 | |
|             self.threadId = threadId
 | |
|             self.threadVariant = threadVariant
 | |
|             self.direction = direction
 | |
|             self.timestampMs = (timestampMs ?? SnodeAPI.currentOffsetTimestampMs())
 | |
|         }
 | |
|         
 | |
|         fileprivate func start(_ db: Database, using dependencies: Dependencies = Dependencies()) {
 | |
|             // Start the typing indicator
 | |
|             switch direction {
 | |
|                 case .outgoing:
 | |
|                     scheduleRefreshCallback(db, shouldSend: (refreshTimer == nil), using: dependencies)
 | |
|                     
 | |
|                 case .incoming:
 | |
|                     try? ThreadTypingIndicator(
 | |
|                         threadId: threadId,
 | |
|                         timestampMs: timestampMs
 | |
|                     )
 | |
|                     .save(db)
 | |
|             }
 | |
|             
 | |
|             // Refresh the timeout since we just started
 | |
|             refreshTimeout()
 | |
|         }
 | |
|         
 | |
|         fileprivate func stop(_ db: Database, using dependencies: Dependencies = Dependencies()) {
 | |
|             self.refreshTimer?.invalidate()
 | |
|             self.refreshTimer = nil
 | |
|             self.stopTimer?.invalidate()
 | |
|             self.stopTimer = nil
 | |
|             
 | |
|             switch direction {
 | |
|                 case .outgoing:
 | |
|                     try? MessageSender.send(
 | |
|                         db,
 | |
|                         message: TypingIndicator(kind: .stopped),
 | |
|                         interactionId: nil,
 | |
|                         threadId: threadId,
 | |
|                         threadVariant: threadVariant,
 | |
|                         using: dependencies
 | |
|                     )
 | |
|                     
 | |
|                 case .incoming:
 | |
|                     _ = try? ThreadTypingIndicator
 | |
|                         .filter(ThreadTypingIndicator.Columns.threadId == self.threadId)
 | |
|                         .deleteAll(db)
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         fileprivate func refreshTimeout() {
 | |
|             let threadId: String = self.threadId
 | |
|             let direction: Direction = self.direction
 | |
|             
 | |
|             // Schedule the 'stopCallback' to cancel the typing indicator
 | |
|             stopTimer?.invalidate()
 | |
|             stopTimer = Timer.scheduledTimerOnMainThread(
 | |
|                 withTimeInterval: (direction == .outgoing ? 3 : 5),
 | |
|                 repeats: false
 | |
|             ) { _ in
 | |
|                 Storage.shared.writeAsync { db in
 | |
|                     TypingIndicators.didStopTyping(db, threadId: threadId, direction: direction)
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         private func scheduleRefreshCallback(
 | |
|             _ db: Database,
 | |
|             shouldSend: Bool = true,
 | |
|             using dependencies: Dependencies
 | |
|         ) {
 | |
|             if shouldSend {
 | |
|                 try? MessageSender.send(
 | |
|                     db,
 | |
|                     message: TypingIndicator(kind: .started),
 | |
|                     interactionId: nil,
 | |
|                     threadId: threadId,
 | |
|                     threadVariant: threadVariant,
 | |
|                     using: dependencies
 | |
|                 )
 | |
|             }
 | |
|             
 | |
|             refreshTimer?.invalidate()
 | |
|             refreshTimer = Timer.scheduledTimerOnMainThread(
 | |
|                 withTimeInterval: 10,
 | |
|                 repeats: false
 | |
|             ) { [weak self] _ in
 | |
|                 dependencies.storage.writeAsync { db in
 | |
|                     self?.scheduleRefreshCallback(db, using: dependencies)
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     // MARK: - Variables
 | |
|     
 | |
|     public static let shared: TypingIndicators = TypingIndicators()
 | |
|     
 | |
|     private static var outgoing: Atomic<[String: Indicator]> = Atomic([:])
 | |
|     private static var incoming: Atomic<[String: Indicator]> = Atomic([:])
 | |
|     
 | |
|     // MARK: - Functions
 | |
|     
 | |
|     public static func didStartTypingNeedsToStart(
 | |
|         threadId: String,
 | |
|         threadVariant: SessionThread.Variant,
 | |
|         threadIsBlocked: Bool,
 | |
|         threadIsMessageRequest: Bool,
 | |
|         direction: Direction,
 | |
|         timestampMs: Int64?
 | |
|     ) -> Bool {
 | |
|         switch direction {
 | |
|             case .outgoing:
 | |
|                 // If we already have an existing typing indicator for this thread then just
 | |
|                 // refresh it's timeout (no need to do anything else)
 | |
|                 if let existingIndicator: Indicator = outgoing.wrappedValue[threadId] {
 | |
|                     existingIndicator.refreshTimeout()
 | |
|                     return false
 | |
|                 }
 | |
|                 
 | |
|                 let newIndicator: Indicator? = Indicator(
 | |
|                     threadId: threadId,
 | |
|                     threadVariant: threadVariant,
 | |
|                     threadIsBlocked: threadIsBlocked,
 | |
|                     threadIsMessageRequest: threadIsMessageRequest,
 | |
|                     direction: direction,
 | |
|                     timestampMs: timestampMs
 | |
|                 )
 | |
|                 newIndicator?.refreshTimeout()
 | |
|                 
 | |
|                 outgoing.mutate { $0[threadId] = newIndicator }
 | |
|                 return true
 | |
|                 
 | |
|             case .incoming:
 | |
|                 // If we already have an existing typing indicator for this thread then just
 | |
|                 // refresh it's timeout (no need to do anything else)
 | |
|                 if let existingIndicator: Indicator = incoming.wrappedValue[threadId] {
 | |
|                     existingIndicator.refreshTimeout()
 | |
|                     return false
 | |
|                 }
 | |
|                 
 | |
|                 let newIndicator: Indicator? = Indicator(
 | |
|                     threadId: threadId,
 | |
|                     threadVariant: threadVariant,
 | |
|                     threadIsBlocked: threadIsBlocked,
 | |
|                     threadIsMessageRequest: threadIsMessageRequest,
 | |
|                     direction: direction,
 | |
|                     timestampMs: timestampMs
 | |
|                 )
 | |
|                 newIndicator?.refreshTimeout()
 | |
|                 
 | |
|                 incoming.mutate { $0[threadId] = newIndicator }
 | |
|                 return true
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     public static func start(_ db: Database, threadId: String, direction: Direction) {
 | |
|         switch direction {
 | |
|             case .outgoing: outgoing.wrappedValue[threadId]?.start(db)
 | |
|             case .incoming: incoming.wrappedValue[threadId]?.start(db)
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     public static func didStopTyping(_ db: Database, threadId: String, direction: Direction) {
 | |
|         switch direction {
 | |
|             case .outgoing:
 | |
|                 if let indicator: Indicator = outgoing.wrappedValue[threadId] {
 | |
|                     indicator.stop(db)
 | |
|                     outgoing.mutate { $0[threadId] = nil }
 | |
|                 }
 | |
|                 
 | |
|             case .incoming:
 | |
|                 if let indicator: Indicator = incoming.wrappedValue[threadId] {
 | |
|                     indicator.stop(db)
 | |
|                     incoming.mutate { $0[threadId] = nil }
 | |
|                 }
 | |
|         }
 | |
|     }
 | |
| }
 |