session-ios/SignalServiceKit/src/Util/TypingIndicators.swift

458 lines
16 KiB
Swift
Raw Normal View History

//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc(OWSTypingIndicators)
public protocol TypingIndicators: class {
@objc
2018-10-31 17:19:07 +01:00
func didStartTypingOutgoingInput(inThread thread: TSThread)
@objc
2018-10-31 17:19:07 +01:00
func didStopTypingOutgoingInput(inThread thread: TSThread)
@objc
func didSendOutgoingMessage(inThread thread: TSThread)
@objc
func didReceiveTypingStartedMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt)
@objc
func didReceiveTypingStoppedMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt)
@objc
func didReceiveIncomingMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt)
2018-10-31 18:24:27 +01:00
// Returns the recipient id of the user who should currently be shown typing for a given thread.
//
// If no one is typing in that thread, returns nil.
// If multiple users are typing in that thread, returns the user to show.
//
// TODO: Use this method.
@objc
2018-11-01 21:19:03 +01:00
func typingRecipientId(forThread thread: TSThread) -> String?
2018-10-31 17:06:02 +01:00
@objc
func setTypingIndicatorsEnabled(value: Bool)
@objc
func areTypingIndicatorsEnabled() -> Bool
}
// MARK: -
@objc(OWSTypingIndicatorsImpl)
public class TypingIndicatorsImpl: NSObject, TypingIndicators {
2018-10-31 17:06:02 +01:00
2018-11-01 15:43:13 +01:00
@objc
public static let typingIndicatorStateDidChange = Notification.Name("typingIndicatorStateDidChange")
2018-10-31 17:06:02 +01:00
private let kDatabaseCollection = "TypingIndicators"
private let kDatabaseKey_TypingIndicatorsEnabled = "kDatabaseKey_TypingIndicatorsEnabled"
private var _areTypingIndicatorsEnabled = false
public override init() {
super.init()
AppReadiness.runNowOrWhenAppWillBecomeReady {
2018-10-31 17:06:02 +01:00
self.setup()
}
}
private func setup() {
AssertIsOnMainThread()
_areTypingIndicatorsEnabled = primaryStorage.dbReadConnection.bool(forKey: kDatabaseKey_TypingIndicatorsEnabled, inCollection: kDatabaseCollection, defaultValue: true)
}
// MARK: - Dependencies
private var primaryStorage: OWSPrimaryStorage {
return SSKEnvironment.shared.primaryStorage
}
private var syncManager: OWSSyncManagerProtocol {
return SSKEnvironment.shared.syncManager
}
2018-10-31 17:06:02 +01:00
// MARK: -
@objc
public func setTypingIndicatorsEnabled(value: Bool) {
AssertIsOnMainThread()
Logger.info("\(_areTypingIndicatorsEnabled) -> \(value)")
2018-10-31 17:06:02 +01:00
_areTypingIndicatorsEnabled = value
primaryStorage.dbReadWriteConnection.setBool(value, forKey: kDatabaseKey_TypingIndicatorsEnabled, inCollection: kDatabaseCollection)
2018-11-01 21:19:03 +01:00
syncManager.sendConfigurationSyncMessage()
NotificationCenter.default.postNotificationNameAsync(TypingIndicatorsImpl.typingIndicatorStateDidChange, object: nil)
2018-10-31 17:06:02 +01:00
}
@objc
public func areTypingIndicatorsEnabled() -> Bool {
AssertIsOnMainThread()
return _areTypingIndicatorsEnabled
}
// MARK: -
@objc
2018-10-31 17:19:07 +01:00
public func didStartTypingOutgoingInput(inThread thread: TSThread) {
AssertIsOnMainThread()
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread) else {
owsFailDebug("Could not locate outgoing indicators state")
return
}
2018-10-31 17:19:07 +01:00
outgoingIndicators.didStartTypingOutgoingInput()
}
@objc
2018-10-31 17:19:07 +01:00
public func didStopTypingOutgoingInput(inThread thread: TSThread) {
AssertIsOnMainThread()
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread) else {
owsFailDebug("Could not locate outgoing indicators state")
return
}
2018-10-31 17:19:07 +01:00
outgoingIndicators.didStopTypingOutgoingInput()
}
@objc
public func didSendOutgoingMessage(inThread thread: TSThread) {
AssertIsOnMainThread()
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread) else {
owsFailDebug("Could not locate outgoing indicators state")
return
}
outgoingIndicators.didSendOutgoingMessage()
}
@objc
public func didReceiveTypingStartedMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt) {
AssertIsOnMainThread()
Logger.info("")
let incomingIndicators = ensureIncomingIndicators(forThread: thread, recipientId: recipientId, deviceId: deviceId)
incomingIndicators.didReceiveTypingStartedMessage()
}
@objc
public func didReceiveTypingStoppedMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt) {
AssertIsOnMainThread()
Logger.info("")
let incomingIndicators = ensureIncomingIndicators(forThread: thread, recipientId: recipientId, deviceId: deviceId)
incomingIndicators.didReceiveTypingStoppedMessage()
}
@objc
public func didReceiveIncomingMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt) {
AssertIsOnMainThread()
Logger.info("")
let incomingIndicators = ensureIncomingIndicators(forThread: thread, recipientId: recipientId, deviceId: deviceId)
incomingIndicators.didReceiveIncomingMessage()
}
@objc
2018-11-01 21:19:03 +01:00
public func typingRecipientId(forThread thread: TSThread) -> String? {
AssertIsOnMainThread()
guard areTypingIndicatorsEnabled() else {
return nil
}
2018-10-31 18:24:27 +01:00
var firstRecipientId: String?
var firstTimestamp: UInt64?
let threadKey = incomingIndicatorsKey(forThread: thread)
guard let deviceMap = incomingIndicatorsMap[threadKey] else {
// No devices are typing in this thread.
return nil
}
for incomingIndicators in deviceMap.values {
2018-10-31 18:24:27 +01:00
guard incomingIndicators.isTyping else {
continue
}
2018-10-31 18:24:27 +01:00
guard let startedTypingTimestamp = incomingIndicators.startedTypingTimestamp else {
owsFailDebug("Typing device is missing start timestamp.")
continue
}
if let firstTimestamp = firstTimestamp,
firstTimestamp < startedTypingTimestamp {
// More than one recipient/device is typing in this conversation;
// prefer the one that started typing first.
continue
}
firstRecipientId = incomingIndicators.recipientId
firstTimestamp = startedTypingTimestamp
}
2018-10-31 18:24:27 +01:00
return firstRecipientId
}
// MARK: -
// Map of thread id-to-OutgoingIndicators.
private var outgoingIndicatorsMap = [String: OutgoingIndicators]()
private func ensureOutgoingIndicators(forThread thread: TSThread) -> OutgoingIndicators? {
AssertIsOnMainThread()
guard let threadId = thread.uniqueId else {
owsFailDebug("Thread missing id")
return nil
}
if let outgoingIndicators = outgoingIndicatorsMap[threadId] {
return outgoingIndicators
}
2018-10-31 17:06:02 +01:00
let outgoingIndicators = OutgoingIndicators(delegate: self, thread: thread)
outgoingIndicatorsMap[threadId] = outgoingIndicators
return outgoingIndicators
}
// The sender maintains two timers per chat:
//
// A sendPause timer
// A sendRefresh timer
private class OutgoingIndicators {
2018-10-31 17:06:02 +01:00
private weak var delegate: TypingIndicators?
private let thread: TSThread
private var sendPauseTimer: Timer?
private var sendRefreshTimer: Timer?
2018-10-31 17:06:02 +01:00
init(delegate: TypingIndicators, thread: TSThread) {
self.delegate = delegate
self.thread = thread
}
// MARK: - Dependencies
private var messageSender: MessageSender {
return SSKEnvironment.shared.messageSender
}
// MARK: -
2018-10-31 17:19:07 +01:00
func didStartTypingOutgoingInput() {
AssertIsOnMainThread()
if sendRefreshTimer == nil {
// If the user types a character into the compose box, and the sendRefresh timer isnt running:
2018-11-01 19:34:05 +01:00
sendTypingMessageIfNecessary(forThread: thread, action: .started)
2018-11-08 17:25:08 +01:00
sendRefreshTimer?.invalidate()
sendRefreshTimer = Timer.weakScheduledTimer(withTimeInterval: 10,
target: self,
selector: #selector(OutgoingIndicators.sendRefreshTimerDidFire),
userInfo: nil,
repeats: false)
} else {
// If the user types a character into the compose box, and the sendRefresh timer is running:
}
sendPauseTimer?.invalidate()
2018-11-08 17:25:08 +01:00
sendPauseTimer = Timer.weakScheduledTimer(withTimeInterval: 3,
target: self,
selector: #selector(OutgoingIndicators.sendPauseTimerDidFire),
userInfo: nil,
repeats: false)
}
2018-10-31 17:19:07 +01:00
func didStopTypingOutgoingInput() {
AssertIsOnMainThread()
2018-11-01 19:34:05 +01:00
sendTypingMessageIfNecessary(forThread: thread, action: .stopped)
2018-11-08 17:25:08 +01:00
sendRefreshTimer?.invalidate()
sendRefreshTimer = nil
2018-11-08 17:25:08 +01:00
sendPauseTimer?.invalidate()
sendPauseTimer = nil
}
@objc
func sendPauseTimerDidFire() {
AssertIsOnMainThread()
2018-11-01 19:34:05 +01:00
sendTypingMessageIfNecessary(forThread: thread, action: .stopped)
2018-11-08 17:25:08 +01:00
sendRefreshTimer?.invalidate()
sendRefreshTimer = nil
2018-11-08 17:25:08 +01:00
sendPauseTimer?.invalidate()
sendPauseTimer = nil
}
@objc
func sendRefreshTimerDidFire() {
AssertIsOnMainThread()
2018-11-01 19:34:05 +01:00
sendTypingMessageIfNecessary(forThread: thread, action: .started)
2018-11-08 17:25:08 +01:00
sendRefreshTimer?.invalidate()
sendRefreshTimer = Timer.weakScheduledTimer(withTimeInterval: 10,
target: self,
selector: #selector(sendRefreshTimerDidFire),
userInfo: nil,
repeats: false)
}
func didSendOutgoingMessage() {
AssertIsOnMainThread()
sendRefreshTimer?.invalidate()
sendRefreshTimer = nil
2018-11-08 17:25:08 +01:00
sendPauseTimer?.invalidate()
sendPauseTimer = nil
}
2018-11-01 19:34:05 +01:00
private func sendTypingMessageIfNecessary(forThread thread: TSThread, action: TypingIndicatorAction) {
Logger.verbose("\(TypingIndicatorMessage.string(forTypingIndicatorAction: action))")
2018-10-31 17:06:02 +01:00
guard let delegate = delegate else {
owsFailDebug("Missing delegate.")
return
}
2018-11-01 19:34:05 +01:00
// `areTypingIndicatorsEnabled` 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.
2018-10-31 17:06:02 +01:00
guard delegate.areTypingIndicatorsEnabled() else {
return
}
let message = TypingIndicatorMessage(thread: thread, action: action)
2018-10-31 17:19:07 +01:00
messageSender.sendPromise(message: message).retainUntilComplete()
}
}
// MARK: -
2018-10-31 18:24:27 +01:00
// Map of (thread id)-to-(recipient id and device id)-to-IncomingIndicators.
private var incomingIndicatorsMap = [String: [String: IncomingIndicators]]()
private func incomingIndicatorsKey(forThread thread: TSThread) -> String {
return String(describing: thread.uniqueId)
}
2018-10-31 18:24:27 +01:00
private func incomingIndicatorsKey(recipientId: String, deviceId: UInt) -> String {
return "\(recipientId) \(deviceId)"
}
private func ensureIncomingIndicators(forThread thread: TSThread, recipientId: String, deviceId: UInt) -> IncomingIndicators {
AssertIsOnMainThread()
2018-10-31 18:24:27 +01:00
let threadKey = incomingIndicatorsKey(forThread: thread)
let deviceKey = incomingIndicatorsKey(recipientId: recipientId, deviceId: deviceId)
guard let deviceMap = incomingIndicatorsMap[threadKey] else {
let incomingIndicators = IncomingIndicators(delegate: self, thread: thread, recipientId: recipientId, deviceId: deviceId)
incomingIndicatorsMap[threadKey] = [deviceKey: incomingIndicators]
return incomingIndicators
}
2018-10-31 18:24:27 +01:00
guard let incomingIndicators = deviceMap[deviceKey] else {
let incomingIndicators = IncomingIndicators(delegate: self, thread: thread, recipientId: recipientId, deviceId: deviceId)
var deviceMapCopy = deviceMap
2018-10-31 18:24:27 +01:00
deviceMapCopy[deviceKey] = incomingIndicators
incomingIndicatorsMap[threadKey] = deviceMapCopy
return incomingIndicators
}
return incomingIndicators
}
// The receiver maintains one timer for each (sender, device) in a chat:
private class IncomingIndicators {
2018-10-31 17:06:02 +01:00
private weak var delegate: TypingIndicators?
2018-10-31 18:24:27 +01:00
private let thread: TSThread
fileprivate let recipientId: String
private let deviceId: UInt
private var displayTypingTimer: Timer?
2018-10-31 18:24:27 +01:00
fileprivate var startedTypingTimestamp: UInt64?
var isTyping = false {
didSet {
AssertIsOnMainThread()
let didChange = oldValue != isTyping
if didChange {
Logger.debug("isTyping changed: \(oldValue) -> \(self.isTyping)")
2018-11-01 19:34:05 +01:00
notifyIfNecessary()
}
}
}
2018-10-31 18:24:27 +01:00
init(delegate: TypingIndicators, thread: TSThread,
recipientId: String, deviceId: UInt) {
2018-10-31 17:06:02 +01:00
self.delegate = delegate
2018-10-31 18:24:27 +01:00
self.thread = thread
self.recipientId = recipientId
self.deviceId = deviceId
}
func didReceiveTypingStartedMessage() {
AssertIsOnMainThread()
displayTypingTimer?.invalidate()
displayTypingTimer = Timer.weakScheduledTimer(withTimeInterval: 15,
target: self,
selector: #selector(IncomingIndicators.displayTypingTimerDidFire),
userInfo: nil,
repeats: false)
2018-10-31 18:24:27 +01:00
if !isTyping {
startedTypingTimestamp = NSDate.ows_millisecondTimeStamp()
}
isTyping = true
}
func didReceiveTypingStoppedMessage() {
AssertIsOnMainThread()
2018-10-31 18:24:27 +01:00
clearTyping()
}
@objc
func displayTypingTimerDidFire() {
AssertIsOnMainThread()
2018-10-31 18:24:27 +01:00
clearTyping()
}
func didReceiveIncomingMessage() {
AssertIsOnMainThread()
2018-10-31 18:24:27 +01:00
clearTyping()
}
private func clearTyping() {
AssertIsOnMainThread()
displayTypingTimer?.invalidate()
displayTypingTimer = nil
2018-10-31 18:24:27 +01:00
startedTypingTimestamp = nil
isTyping = false
}
2018-11-01 19:34:05 +01:00
private func notifyIfNecessary() {
Logger.verbose("")
2018-10-31 17:06:02 +01:00
guard let delegate = delegate else {
owsFailDebug("Missing delegate.")
return
}
2018-11-01 19:34:05 +01:00
// `areTypingIndicatorsEnabled` 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.
2018-10-31 17:06:02 +01:00
guard delegate.areTypingIndicatorsEnabled() else {
return
}
2018-10-31 18:24:27 +01:00
guard let threadId = thread.uniqueId else {
owsFailDebug("Thread is missing id.")
return
}
NotificationCenter.default.postNotificationNameAsync(TypingIndicatorsImpl.typingIndicatorStateDidChange, object: threadId)
}
}
}