mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Start work on typing indicators.
* Update proto schema to reflect typing indicators. * Sketch out OWSTypingIndicatorMessage. * Add "online" to the service message params. * Sketch out logic to send typing indicator messages. * Sketch out OWSTypingIndicators class.
This commit is contained in:
parent
40aa78e001
commit
a98c82645c
22 changed files with 964 additions and 29 deletions
2
Pods
2
Pods
|
@ -1 +1 @@
|
|||
Subproject commit 1776400ae37dff8b873845a14a10fefe9c89b675
|
||||
Subproject commit 5821eb8e9fd9f76fbdbedd3402892c185e65c48e
|
|
@ -348,7 +348,6 @@
|
|||
455A16DE1F1FEA0000F86704 /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 455A16DC1F1FEA0000F86704 /* MetalKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||
455AC69E1F4F8B0300134004 /* ImageCacheTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 455AC69D1F4F8B0300134004 /* ImageCacheTest.swift */; };
|
||||
45638BDC1F3DD0D400128435 /* DebugUICalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45638BDB1F3DD0D400128435 /* DebugUICalling.swift */; };
|
||||
45638BDF1F3DDB2200128435 /* MessageSender+Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45638BDE1F3DDB2200128435 /* MessageSender+Promise.swift */; };
|
||||
45666F581D9B2880008FE134 /* OWSScrubbingLogFormatterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 45666F571D9B2880008FE134 /* OWSScrubbingLogFormatterTest.m */; };
|
||||
456F6E2F1E261D1000FD2210 /* PeerConnectionClientTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456F6E2E1E261D1000FD2210 /* PeerConnectionClientTest.swift */; };
|
||||
4574A5D61DD6704700C6B692 /* CallService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4574A5D51DD6704700C6B692 /* CallService.swift */; };
|
||||
|
@ -1017,7 +1016,6 @@
|
|||
455A16DC1F1FEA0000F86704 /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; };
|
||||
455AC69D1F4F8B0300134004 /* ImageCacheTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCacheTest.swift; sourceTree = "<group>"; };
|
||||
45638BDB1F3DD0D400128435 /* DebugUICalling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugUICalling.swift; sourceTree = "<group>"; };
|
||||
45638BDE1F3DDB2200128435 /* MessageSender+Promise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageSender+Promise.swift"; sourceTree = "<group>"; };
|
||||
45666EC41D99483D008FE134 /* OWSAvatarBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAvatarBuilder.h; sourceTree = "<group>"; };
|
||||
45666EC51D99483D008FE134 /* OWSAvatarBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAvatarBuilder.m; sourceTree = "<group>"; };
|
||||
45666EC71D994C0D008FE134 /* OWSGroupAvatarBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSGroupAvatarBuilder.h; sourceTree = "<group>"; };
|
||||
|
@ -2173,6 +2171,7 @@
|
|||
76EB04C818170B33006006FC /* util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */,
|
||||
B90418E4183E9DD40038554A /* DateUtil.h */,
|
||||
B90418E5183E9DD40038554A /* DateUtil.m */,
|
||||
34B0796C1FCF46B000E248C2 /* MainAppContext.h */,
|
||||
|
@ -2201,7 +2200,6 @@
|
|||
34E5DC8120D8050D00C08145 /* RegistrationUtils.m */,
|
||||
4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */,
|
||||
FCFA64B11A24F29E0007FB87 /* UI Categories */,
|
||||
4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */,
|
||||
);
|
||||
path = util;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2465,7 +2463,6 @@
|
|||
FCFA64B11A24F29E0007FB87 /* UI Categories */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
45638BDE1F3DDB2200128435 /* MessageSender+Promise.swift */,
|
||||
45C0DC1A1E68FE9000E04C47 /* UIApplication+OWS.swift */,
|
||||
45C0DC1D1E69011F00E04C47 /* UIStoryboard+OWS.swift */,
|
||||
EF764C331DB67CC5000D9A87 /* UIViewController+Permissions.h */,
|
||||
|
@ -3395,7 +3392,6 @@
|
|||
45AE48511E0732D6004D96C2 /* TurnServerInfo.swift in Sources */,
|
||||
34B3F8771E8DF1700035BE1A /* ContactsPicker.swift in Sources */,
|
||||
45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */,
|
||||
45638BDF1F3DDB2200128435 /* MessageSender+Promise.swift in Sources */,
|
||||
45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */,
|
||||
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */,
|
||||
45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */,
|
||||
|
|
|
@ -14,6 +14,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (void)inputTextViewSendMessagePressed;
|
||||
|
||||
- (void)inputTextViewDidChangeText;
|
||||
|
||||
- (void)textViewDidChange:(UITextView *)textView;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
|
|
@ -65,6 +65,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return self;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)setFont:(UIFont *_Nullable)font
|
||||
{
|
||||
[super setFont:font];
|
||||
|
@ -176,10 +178,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (void)textViewDidChange:(UITextView *)textView
|
||||
{
|
||||
OWSAssertDebug(self.inputTextViewDelegate);
|
||||
OWSAssertDebug(self.textViewToolbarDelegate);
|
||||
|
||||
[self updatePlaceholderVisibility];
|
||||
|
||||
[self.inputTextViewDelegate textViewDidChange:self];
|
||||
[self.textViewToolbarDelegate textViewDidChange:self];
|
||||
}
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@
|
|||
#import <SignalServiceKit/OWSPrimaryStorage.h>
|
||||
#import <SignalServiceKit/OWSReadReceiptManager.h>
|
||||
#import <SignalServiceKit/OWSVerificationStateChangeMessage.h>
|
||||
#import <SignalServiceKit/SignalServiceKit-Swift.h>
|
||||
#import <SignalServiceKit/TSAccountManager.h>
|
||||
#import <SignalServiceKit/TSGroupModel.h>
|
||||
#import <SignalServiceKit/TSInvalidIdentityKeyReceivingErrorMessage.h>
|
||||
|
@ -199,13 +200,6 @@ typedef enum : NSUInteger {
|
|||
|
||||
@property (nonatomic) BOOL peek;
|
||||
|
||||
@property (nonatomic, readonly) OWSContactsManager *contactsManager;
|
||||
@property (nonatomic, readonly) ContactsUpdater *contactsUpdater;
|
||||
@property (nonatomic, readonly) OWSMessageSender *messageSender;
|
||||
@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage;
|
||||
@property (nonatomic, readonly) TSNetworkManager *networkManager;
|
||||
@property (nonatomic, readonly) OutboundCallInitiator *outboundCallInitiator;
|
||||
@property (nonatomic, readonly) OWSBlockingManager *blockingManager;
|
||||
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
|
||||
|
||||
@property (nonatomic) BOOL userHasScrolled;
|
||||
|
@ -274,13 +268,6 @@ typedef enum : NSUInteger {
|
|||
- (void)commonInit
|
||||
{
|
||||
_viewControllerCreatedAt = CACurrentMediaTime();
|
||||
_contactsManager = Environment.shared.contactsManager;
|
||||
_contactsUpdater = SSKEnvironment.shared.contactsUpdater;
|
||||
_messageSender = SSKEnvironment.shared.messageSender;
|
||||
_outboundCallInitiator = AppEnvironment.shared.outboundCallInitiator;
|
||||
_primaryStorage = [OWSPrimaryStorage sharedManager];
|
||||
_networkManager = [TSNetworkManager sharedManager];
|
||||
_blockingManager = [OWSBlockingManager sharedManager];
|
||||
_contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self];
|
||||
_contactShareViewHelper = [[ContactShareViewHelper alloc] initWithContactsManager:self.contactsManager];
|
||||
_contactShareViewHelper.delegate = self;
|
||||
|
@ -306,7 +293,47 @@ typedef enum : NSUInteger {
|
|||
return Environment.shared.audioSession;
|
||||
}
|
||||
|
||||
#pragma mark
|
||||
- (OWSMessageSender *)messageSender
|
||||
{
|
||||
return SSKEnvironment.shared.messageSender;
|
||||
}
|
||||
|
||||
- (OWSContactsManager *)contactsManager
|
||||
{
|
||||
return Environment.shared.contactsManager;
|
||||
}
|
||||
|
||||
- (ContactsUpdater *)contactsUpdater
|
||||
{
|
||||
return SSKEnvironment.shared.contactsUpdater;
|
||||
}
|
||||
|
||||
- (OWSBlockingManager *)blockingManager
|
||||
{
|
||||
return [OWSBlockingManager sharedManager];
|
||||
}
|
||||
|
||||
- (OWSPrimaryStorage *)primaryStorage
|
||||
{
|
||||
return SSKEnvironment.shared.primaryStorage;
|
||||
}
|
||||
|
||||
- (TSNetworkManager *)networkManager
|
||||
{
|
||||
return SSKEnvironment.shared.networkManager;
|
||||
}
|
||||
|
||||
- (OutboundCallInitiator *)outboundCallInitiator
|
||||
{
|
||||
return AppEnvironment.shared.outboundCallInitiator;
|
||||
}
|
||||
|
||||
- (id<OWSTypingIndicators>)typingIndicators
|
||||
{
|
||||
return SSKEnvironment.shared.typingIndicators;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)addNotificationListeners
|
||||
{
|
||||
|
@ -1108,7 +1135,7 @@ typedef enum : NSUInteger {
|
|||
|
||||
TSGroupThread *groupThread = (TSGroupThread *)self.thread;
|
||||
int blockedMemberCount = 0;
|
||||
NSArray<NSString *> *blockedPhoneNumbers = [_blockingManager blockedPhoneNumbers];
|
||||
NSArray<NSString *> *blockedPhoneNumbers = [self.blockingManager blockedPhoneNumbers];
|
||||
for (NSString *contactIdentifier in groupThread.groupModel.groupMemberIds) {
|
||||
if ([blockedPhoneNumbers containsObject:contactIdentifier]) {
|
||||
blockedMemberCount++;
|
||||
|
@ -2813,6 +2840,7 @@ typedef enum : NSUInteger {
|
|||
SystemSoundID soundId = [OWSSounds systemSoundIDForSound:OWSSound_MessageSent quiet:YES];
|
||||
AudioServicesPlaySystemSound(soundId);
|
||||
}
|
||||
[self.typingIndicators didSendOutgoingMessageInThread:self.thread];
|
||||
}
|
||||
|
||||
#pragma mark UIDocumentMenuDelegate
|
||||
|
@ -2885,7 +2913,7 @@ typedef enum : NSUInteger {
|
|||
|
||||
[dataSource setSourceFilename:filename];
|
||||
|
||||
// Although we want to be able to send higher quality attachments throught the document picker
|
||||
// Although we want to be able to send higher quality attachments through the document picker
|
||||
// it's more imporant that we ensure the sent format is one all clients can accept (e.g. *not* quicktime .mov)
|
||||
if ([SignalAttachment isInvalidVideoWithDataSource:dataSource dataUTI:type]) {
|
||||
[self sendQualityAdjustedAttachmentForVideo:url filename:filename skipApprovalDialog:NO];
|
||||
|
@ -3745,6 +3773,7 @@ typedef enum : NSUInteger {
|
|||
[self stopRecording];
|
||||
self.audioRecorder = nil;
|
||||
self.voiceMessageUUID = nil;
|
||||
[self.typingIndicators didStopTypeOutgoingInputInThread:self.thread];
|
||||
}
|
||||
|
||||
- (void)setAudioRecorder:(nullable AVAudioRecorder *)audioRecorder
|
||||
|
@ -3854,6 +3883,7 @@ typedef enum : NSUInteger {
|
|||
|
||||
[self dismissKeyBoard];
|
||||
[self presentViewController:actionSheetController animated:true completion:nil];
|
||||
[self.typingIndicators didStartTypeOutgoingInputInThread:self.thread];
|
||||
}
|
||||
|
||||
- (nullable NSIndexPath *)lastVisibleIndexPath
|
||||
|
@ -4105,6 +4135,15 @@ typedef enum : NSUInteger {
|
|||
|
||||
#pragma mark - ConversationInputTextViewDelegate
|
||||
|
||||
- (void)textViewDidChange:(UITextView *)textView
|
||||
{
|
||||
if (textView.text.length > 0) {
|
||||
[self.typingIndicators didStartTypeOutgoingInputInThread:self.thread];
|
||||
} else {
|
||||
[self.typingIndicators didStopTypeOutgoingInputInThread:self.thread];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)inputTextViewSendMessagePressed
|
||||
{
|
||||
[self sendButtonPressed];
|
||||
|
@ -4521,6 +4560,7 @@ typedef enum : NSUInteger {
|
|||
[self.inputToolbar showVoiceMemoUI];
|
||||
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
|
||||
[self requestRecordingVoiceMemo];
|
||||
[self.typingIndicators didStartTypeOutgoingInputInThread:self.thread];
|
||||
}
|
||||
|
||||
- (void)voiceMemoGestureDidEnd
|
||||
|
|
|
@ -85,6 +85,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[[OWSOutgoingReceiptManager alloc] initWithPrimaryStorage:primaryStorage];
|
||||
OWSSyncManager *syncManager = [[OWSSyncManager alloc] initDefault];
|
||||
id<SSKReachabilityManager> reachabilityManager = [SSKReachabilityManagerImpl new];
|
||||
id<OWSTypingIndicators> typingIndicators = [[OWSTypingIndicatorsImpl alloc] init];
|
||||
|
||||
OWSAudioSession *audioSession = [OWSAudioSession new];
|
||||
OWSSounds *sounds = [[OWSSounds alloc] initWithPrimaryStorage:primaryStorage];
|
||||
id<OWSProximityMonitoringManager> proximityMonitoringManager = [OWSProximityMonitoringManagerImpl new];
|
||||
|
@ -120,7 +122,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
readReceiptManager:readReceiptManager
|
||||
outgoingReceiptManager:outgoingReceiptManager
|
||||
reachabilityManager:reachabilityManager
|
||||
syncManager:syncManager]];
|
||||
syncManager:syncManager
|
||||
typingIndicators:typingIndicators]];
|
||||
|
||||
appSpecificSingletonBlock();
|
||||
|
||||
|
|
|
@ -39,12 +39,26 @@ message Envelope {
|
|||
optional uint64 serverTimestamp = 10;
|
||||
}
|
||||
|
||||
message TypingMessage {
|
||||
enum Action {
|
||||
STARTED = 0;
|
||||
STOPPED = 1;
|
||||
}
|
||||
|
||||
// @required
|
||||
optional uint64 timestamp = 1;
|
||||
// @required
|
||||
optional Action action = 2;
|
||||
optional bytes groupId = 3;
|
||||
}
|
||||
|
||||
message Content {
|
||||
optional DataMessage dataMessage = 1;
|
||||
optional SyncMessage syncMessage = 2;
|
||||
optional CallMessage callMessage = 3;
|
||||
optional NullMessage nullMessage = 4;
|
||||
optional ReceiptMessage receiptMessage = 5;
|
||||
optional TypingMessage typingMessage = 6;
|
||||
}
|
||||
|
||||
message CallMessage {
|
||||
|
|
|
@ -130,6 +130,8 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
|
|||
|
||||
@property (nonatomic, readonly) BOOL isSilent;
|
||||
|
||||
@property (nonatomic, readonly) BOOL isOnline;
|
||||
|
||||
/**
|
||||
* The data representation of this message, to be encrypted, before being sent.
|
||||
*/
|
||||
|
|
|
@ -495,6 +495,11 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
|
|||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isOnline
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (OWSInteractionType)interactionType
|
||||
{
|
||||
return OWSInteractionType_OutgoingMessage;
|
||||
|
|
|
@ -159,6 +159,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return SSKEnvironment.shared.profileManager;
|
||||
}
|
||||
|
||||
- (id<OWSTypingIndicators>)typingIndicators
|
||||
{
|
||||
return SSKEnvironment.shared.typingIndicators;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)startObserving
|
||||
|
@ -407,6 +412,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[self handleIncomingEnvelope:envelope withDataMessage:contentProto.dataMessage transaction:transaction];
|
||||
} else if (contentProto.callMessage) {
|
||||
[self handleIncomingEnvelope:envelope withCallMessage:contentProto.callMessage];
|
||||
} else if (contentProto.typingMessage) {
|
||||
[self handleIncomingEnvelope:envelope withTypingMessage:contentProto.typingMessage transaction:transaction];
|
||||
} else if (contentProto.nullMessage) {
|
||||
OWSLogInfo(@"Received null message.");
|
||||
} else if (contentProto.receiptMessage) {
|
||||
|
@ -642,6 +649,53 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
});
|
||||
}
|
||||
|
||||
- (void)handleIncomingEnvelope:(SSKProtoEnvelope *)envelope
|
||||
withTypingMessage:(SSKProtoTypingMessage *)typingMessage
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
OWSAssertDebug(transaction);
|
||||
|
||||
if (!envelope) {
|
||||
OWSFailDebug(@"Missing envelope.");
|
||||
return;
|
||||
}
|
||||
if (!typingMessage) {
|
||||
OWSFailDebug(@"Missing typingMessage.");
|
||||
return;
|
||||
}
|
||||
|
||||
TSThread *_Nullable thread;
|
||||
if (typingMessage.hasGroupID) {
|
||||
thread = [TSGroupThread threadWithGroupId:typingMessage.groupID transaction:transaction];
|
||||
} else {
|
||||
thread = [TSContactThread getThreadWithContactId:envelope.source transaction:transaction];
|
||||
}
|
||||
if (!thread) {
|
||||
// This isn't neccesarily an error. We might not yet know about the thread,
|
||||
// in which case we don't need to display the typing indicators.
|
||||
OWSLogWarn(@"Could not locate thread for typingMessage.");
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
switch (typingMessage.action) {
|
||||
case SSKProtoTypingMessageActionStarted:
|
||||
[self.typingIndicators didReceiveTypingStartedMessageInThread:thread
|
||||
recipientId:envelope.source
|
||||
deviceId:envelope.sourceDevice];
|
||||
break;
|
||||
case SSKProtoTypingMessageActionStopped:
|
||||
[self.typingIndicators didReceiveTypingStoppedMessageInThread:thread
|
||||
recipientId:envelope.source
|
||||
deviceId:envelope.sourceDevice];
|
||||
break;
|
||||
default:
|
||||
OWSFailDebug(@"Typing message has unexpected action.");
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)handleReceivedGroupAvatarUpdateWithEnvelope:(SSKProtoEnvelope *)envelope
|
||||
dataMessage:(SSKProtoDataMessage *)dataMessage
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
|
@ -1352,6 +1406,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
inThread:thread
|
||||
contactsManager:self.contactsManager
|
||||
transaction:transaction];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.typingIndicators didReceiveIncomingMessageInThread:thread
|
||||
recipientId:envelope.source
|
||||
deviceId:envelope.sourceDevice];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - helpers
|
||||
|
|
|
@ -1648,12 +1648,14 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
}
|
||||
|
||||
BOOL isSilent = message.isSilent;
|
||||
BOOL isOnline = message.isOnline;
|
||||
OWSMessageServiceParams *messageParams =
|
||||
[[OWSMessageServiceParams alloc] initWithType:messageType
|
||||
recipientId:recipientId
|
||||
device:[deviceId intValue]
|
||||
content:serializedMessage
|
||||
isSilent:isSilent
|
||||
isOnline:isOnline
|
||||
registrationId:[cipher throws_remoteRegistrationId:transaction]];
|
||||
|
||||
NSError *error;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TSConstants.h"
|
||||
|
@ -20,12 +20,14 @@
|
|||
@property (nonatomic, readonly) int destinationRegistrationId;
|
||||
@property (nonatomic, readonly) NSString *content;
|
||||
@property (nonatomic, readonly) BOOL silent;
|
||||
@property (nonatomic, readonly) BOOL online;
|
||||
|
||||
- (instancetype)initWithType:(TSWhisperMessageType)type
|
||||
recipientId:(NSString *)destination
|
||||
device:(int)deviceId
|
||||
content:(NSData *)content
|
||||
isSilent:(BOOL)isSilent
|
||||
isOnline:(BOOL)isOnline
|
||||
registrationId:(int)registrationId;
|
||||
|
||||
@end
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
device:(int)deviceId
|
||||
content:(NSData *)content
|
||||
isSilent:(BOOL)isSilent
|
||||
isOnline:(BOOL)isOnline
|
||||
registrationId:(int)registrationId
|
||||
{
|
||||
self = [super init];
|
||||
|
@ -32,6 +33,7 @@
|
|||
_destinationRegistrationId = registrationId;
|
||||
_content = [content base64EncodedString];
|
||||
_silent = isSilent;
|
||||
_online = isOnline;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
|
118
SignalServiceKit/src/Messages/TypingIndicatorMessage.swift
Normal file
118
SignalServiceKit/src/Messages/TypingIndicatorMessage.swift
Normal file
|
@ -0,0 +1,118 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc(OWSTypingIndicatorAction)
|
||||
public enum TypingIndicatorAction: Int {
|
||||
case started
|
||||
case stopped
|
||||
}
|
||||
|
||||
@objc(OWSTypingIndicatorMessage)
|
||||
public class TypingIndicatorMessage: TSOutgoingMessage {
|
||||
private let action: TypingIndicatorAction
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
@objc
|
||||
public init(thread: TSThread,
|
||||
action: TypingIndicatorAction) {
|
||||
self.action = action
|
||||
|
||||
super.init(outgoingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(),
|
||||
in: thread,
|
||||
messageBody: nil,
|
||||
attachmentIds: NSMutableArray(),
|
||||
expiresInSeconds: 0,
|
||||
expireStartedAt: 0,
|
||||
isVoiceMessage: false,
|
||||
groupMetaMessage: .unspecified,
|
||||
quotedMessage: nil,
|
||||
contactShare: nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
public required init!(coder: NSCoder) {
|
||||
self.action = .started
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
@objc
|
||||
public required init(dictionary dictionaryValue: [AnyHashable: Any]!) throws {
|
||||
self.action = .started
|
||||
try super.init(dictionary: dictionaryValue)
|
||||
}
|
||||
|
||||
@objc
|
||||
public override func shouldSyncTranscript() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@objc
|
||||
public override var isSilent: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@objc
|
||||
public override var isOnline: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
private func protoAction(forAction action: TypingIndicatorAction) -> SSKProtoTypingMessage.SSKProtoTypingMessageAction {
|
||||
switch action {
|
||||
case .started:
|
||||
return .started
|
||||
case .stopped:
|
||||
return .stopped
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public override func buildPlainTextData(_ recipient: SignalRecipient) -> Data? {
|
||||
|
||||
let typingBuilder = SSKProtoTypingMessage.builder(timestamp: self.timestamp,
|
||||
action: protoAction(forAction: action))
|
||||
|
||||
if let groupThread = self.thread as? TSGroupThread {
|
||||
typingBuilder.setGroupID(groupThread.groupModel.groupId)
|
||||
}
|
||||
|
||||
let contentBuilder = SSKProtoContent.builder()
|
||||
|
||||
do {
|
||||
contentBuilder.setTypingMessage(try typingBuilder.build())
|
||||
|
||||
let data = try contentBuilder.buildSerializedData()
|
||||
return data
|
||||
} catch let error {
|
||||
owsFailDebug("failed to build content: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: TSYapDatabaseObject overrides
|
||||
|
||||
@objc
|
||||
public override func shouldBeSaved() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@objc
|
||||
public override var debugDescription: String {
|
||||
return "typingIndicatorMessage"
|
||||
}
|
||||
|
||||
// MARK:
|
||||
|
||||
@objc(stringForTypingIndicatorAction:)
|
||||
public class func string(forTypingIndicatorAction action: TypingIndicatorAction) -> String {
|
||||
switch action {
|
||||
case .started:
|
||||
return "started"
|
||||
case .stopped:
|
||||
return "stopped"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -268,6 +268,156 @@ extension SSKProtoEnvelope.SSKProtoEnvelopeBuilder {
|
|||
|
||||
#endif
|
||||
|
||||
// MARK: - SSKProtoTypingMessage
|
||||
|
||||
@objc public class SSKProtoTypingMessage: NSObject {
|
||||
|
||||
// MARK: - SSKProtoTypingMessageAction
|
||||
|
||||
@objc public enum SSKProtoTypingMessageAction: Int32 {
|
||||
case started = 0
|
||||
case stopped = 1
|
||||
}
|
||||
|
||||
private class func SSKProtoTypingMessageActionWrap(_ value: SignalServiceProtos_TypingMessage.Action) -> SSKProtoTypingMessageAction {
|
||||
switch value {
|
||||
case .started: return .started
|
||||
case .stopped: return .stopped
|
||||
}
|
||||
}
|
||||
|
||||
private class func SSKProtoTypingMessageActionUnwrap(_ value: SSKProtoTypingMessageAction) -> SignalServiceProtos_TypingMessage.Action {
|
||||
switch value {
|
||||
case .started: return .started
|
||||
case .stopped: return .stopped
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SSKProtoTypingMessageBuilder
|
||||
|
||||
@objc public class func builder(timestamp: UInt64, action: SSKProtoTypingMessageAction) -> SSKProtoTypingMessageBuilder {
|
||||
return SSKProtoTypingMessageBuilder(timestamp: timestamp, action: action)
|
||||
}
|
||||
|
||||
// asBuilder() constructs a builder that reflects the proto's contents.
|
||||
@objc public func asBuilder() -> SSKProtoTypingMessageBuilder {
|
||||
let builder = SSKProtoTypingMessageBuilder(timestamp: timestamp, action: action)
|
||||
if let _value = groupID {
|
||||
builder.setGroupID(_value)
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
@objc public class SSKProtoTypingMessageBuilder: NSObject {
|
||||
|
||||
private var proto = SignalServiceProtos_TypingMessage()
|
||||
|
||||
@objc fileprivate override init() {}
|
||||
|
||||
@objc fileprivate init(timestamp: UInt64, action: SSKProtoTypingMessageAction) {
|
||||
super.init()
|
||||
|
||||
setTimestamp(timestamp)
|
||||
setAction(action)
|
||||
}
|
||||
|
||||
@objc public func setTimestamp(_ valueParam: UInt64) {
|
||||
proto.timestamp = valueParam
|
||||
}
|
||||
|
||||
@objc public func setAction(_ valueParam: SSKProtoTypingMessageAction) {
|
||||
proto.action = SSKProtoTypingMessageActionUnwrap(valueParam)
|
||||
}
|
||||
|
||||
@objc public func setGroupID(_ valueParam: Data) {
|
||||
proto.groupID = valueParam
|
||||
}
|
||||
|
||||
@objc public func build() throws -> SSKProtoTypingMessage {
|
||||
return try SSKProtoTypingMessage.parseProto(proto)
|
||||
}
|
||||
|
||||
@objc public func buildSerializedData() throws -> Data {
|
||||
return try SSKProtoTypingMessage.parseProto(proto).serializedData()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate let proto: SignalServiceProtos_TypingMessage
|
||||
|
||||
@objc public let timestamp: UInt64
|
||||
|
||||
@objc public let action: SSKProtoTypingMessageAction
|
||||
|
||||
@objc public var groupID: Data? {
|
||||
guard proto.hasGroupID else {
|
||||
return nil
|
||||
}
|
||||
return proto.groupID
|
||||
}
|
||||
@objc public var hasGroupID: Bool {
|
||||
return proto.hasGroupID
|
||||
}
|
||||
|
||||
private init(proto: SignalServiceProtos_TypingMessage,
|
||||
timestamp: UInt64,
|
||||
action: SSKProtoTypingMessageAction) {
|
||||
self.proto = proto
|
||||
self.timestamp = timestamp
|
||||
self.action = action
|
||||
}
|
||||
|
||||
@objc
|
||||
public func serializedData() throws -> Data {
|
||||
return try self.proto.serializedData()
|
||||
}
|
||||
|
||||
@objc public class func parseData(_ serializedData: Data) throws -> SSKProtoTypingMessage {
|
||||
let proto = try SignalServiceProtos_TypingMessage(serializedData: serializedData)
|
||||
return try parseProto(proto)
|
||||
}
|
||||
|
||||
fileprivate class func parseProto(_ proto: SignalServiceProtos_TypingMessage) throws -> SSKProtoTypingMessage {
|
||||
guard proto.hasTimestamp else {
|
||||
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: timestamp")
|
||||
}
|
||||
let timestamp = proto.timestamp
|
||||
|
||||
guard proto.hasAction else {
|
||||
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: action")
|
||||
}
|
||||
let action = SSKProtoTypingMessageActionWrap(proto.action)
|
||||
|
||||
// MARK: - Begin Validation Logic for SSKProtoTypingMessage -
|
||||
|
||||
// MARK: - End Validation Logic for SSKProtoTypingMessage -
|
||||
|
||||
let result = SSKProtoTypingMessage(proto: proto,
|
||||
timestamp: timestamp,
|
||||
action: action)
|
||||
return result
|
||||
}
|
||||
|
||||
@objc public override var debugDescription: String {
|
||||
return "\(proto)"
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
extension SSKProtoTypingMessage {
|
||||
@objc public func serializedDataIgnoringErrors() -> Data? {
|
||||
return try! self.serializedData()
|
||||
}
|
||||
}
|
||||
|
||||
extension SSKProtoTypingMessage.SSKProtoTypingMessageBuilder {
|
||||
@objc public func buildIgnoringErrors() -> SSKProtoTypingMessage? {
|
||||
return try! self.build()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// MARK: - SSKProtoContent
|
||||
|
||||
@objc public class SSKProtoContent: NSObject {
|
||||
|
@ -296,6 +446,9 @@ extension SSKProtoEnvelope.SSKProtoEnvelopeBuilder {
|
|||
if let _value = receiptMessage {
|
||||
builder.setReceiptMessage(_value)
|
||||
}
|
||||
if let _value = typingMessage {
|
||||
builder.setTypingMessage(_value)
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
|
@ -325,6 +478,10 @@ extension SSKProtoEnvelope.SSKProtoEnvelopeBuilder {
|
|||
proto.receiptMessage = valueParam.proto
|
||||
}
|
||||
|
||||
@objc public func setTypingMessage(_ valueParam: SSKProtoTypingMessage) {
|
||||
proto.typingMessage = valueParam.proto
|
||||
}
|
||||
|
||||
@objc public func build() throws -> SSKProtoContent {
|
||||
return try SSKProtoContent.parseProto(proto)
|
||||
}
|
||||
|
@ -346,18 +503,22 @@ extension SSKProtoEnvelope.SSKProtoEnvelopeBuilder {
|
|||
|
||||
@objc public let receiptMessage: SSKProtoReceiptMessage?
|
||||
|
||||
@objc public let typingMessage: SSKProtoTypingMessage?
|
||||
|
||||
private init(proto: SignalServiceProtos_Content,
|
||||
dataMessage: SSKProtoDataMessage?,
|
||||
syncMessage: SSKProtoSyncMessage?,
|
||||
callMessage: SSKProtoCallMessage?,
|
||||
nullMessage: SSKProtoNullMessage?,
|
||||
receiptMessage: SSKProtoReceiptMessage?) {
|
||||
receiptMessage: SSKProtoReceiptMessage?,
|
||||
typingMessage: SSKProtoTypingMessage?) {
|
||||
self.proto = proto
|
||||
self.dataMessage = dataMessage
|
||||
self.syncMessage = syncMessage
|
||||
self.callMessage = callMessage
|
||||
self.nullMessage = nullMessage
|
||||
self.receiptMessage = receiptMessage
|
||||
self.typingMessage = typingMessage
|
||||
}
|
||||
|
||||
@objc
|
||||
|
@ -396,6 +557,11 @@ extension SSKProtoEnvelope.SSKProtoEnvelopeBuilder {
|
|||
receiptMessage = try SSKProtoReceiptMessage.parseProto(proto.receiptMessage)
|
||||
}
|
||||
|
||||
var typingMessage: SSKProtoTypingMessage? = nil
|
||||
if proto.hasTypingMessage {
|
||||
typingMessage = try SSKProtoTypingMessage.parseProto(proto.typingMessage)
|
||||
}
|
||||
|
||||
// MARK: - Begin Validation Logic for SSKProtoContent -
|
||||
|
||||
// MARK: - End Validation Logic for SSKProtoContent -
|
||||
|
@ -405,7 +571,8 @@ extension SSKProtoEnvelope.SSKProtoEnvelopeBuilder {
|
|||
syncMessage: syncMessage,
|
||||
callMessage: callMessage,
|
||||
nullMessage: nullMessage,
|
||||
receiptMessage: receiptMessage)
|
||||
receiptMessage: receiptMessage,
|
||||
typingMessage: typingMessage)
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
|
@ -180,6 +180,83 @@ extension SignalServiceProtos_Envelope.TypeEnum: CaseIterable {
|
|||
|
||||
#endif // swift(>=4.2)
|
||||
|
||||
struct SignalServiceProtos_TypingMessage {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
/// @required
|
||||
var timestamp: UInt64 {
|
||||
get {return _timestamp ?? 0}
|
||||
set {_timestamp = newValue}
|
||||
}
|
||||
/// Returns true if `timestamp` has been explicitly set.
|
||||
var hasTimestamp: Bool {return self._timestamp != nil}
|
||||
/// Clears the value of `timestamp`. Subsequent reads from it will return its default value.
|
||||
mutating func clearTimestamp() {self._timestamp = nil}
|
||||
|
||||
/// @required
|
||||
var action: SignalServiceProtos_TypingMessage.Action {
|
||||
get {return _action ?? .started}
|
||||
set {_action = newValue}
|
||||
}
|
||||
/// Returns true if `action` has been explicitly set.
|
||||
var hasAction: Bool {return self._action != nil}
|
||||
/// Clears the value of `action`. Subsequent reads from it will return its default value.
|
||||
mutating func clearAction() {self._action = nil}
|
||||
|
||||
var groupID: Data {
|
||||
get {return _groupID ?? SwiftProtobuf.Internal.emptyData}
|
||||
set {_groupID = newValue}
|
||||
}
|
||||
/// Returns true if `groupID` has been explicitly set.
|
||||
var hasGroupID: Bool {return self._groupID != nil}
|
||||
/// Clears the value of `groupID`. Subsequent reads from it will return its default value.
|
||||
mutating func clearGroupID() {self._groupID = nil}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
enum Action: SwiftProtobuf.Enum {
|
||||
typealias RawValue = Int
|
||||
case started // = 0
|
||||
case stopped // = 1
|
||||
|
||||
init() {
|
||||
self = .started
|
||||
}
|
||||
|
||||
init?(rawValue: Int) {
|
||||
switch rawValue {
|
||||
case 0: self = .started
|
||||
case 1: self = .stopped
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var rawValue: Int {
|
||||
switch self {
|
||||
case .started: return 0
|
||||
case .stopped: return 1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
init() {}
|
||||
|
||||
fileprivate var _timestamp: UInt64? = nil
|
||||
fileprivate var _action: SignalServiceProtos_TypingMessage.Action? = nil
|
||||
fileprivate var _groupID: Data? = nil
|
||||
}
|
||||
|
||||
#if swift(>=4.2)
|
||||
|
||||
extension SignalServiceProtos_TypingMessage.Action: CaseIterable {
|
||||
// Support synthesized by the compiler.
|
||||
}
|
||||
|
||||
#endif // swift(>=4.2)
|
||||
|
||||
struct SignalServiceProtos_Content {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
|
@ -230,6 +307,15 @@ struct SignalServiceProtos_Content {
|
|||
/// Clears the value of `receiptMessage`. Subsequent reads from it will return its default value.
|
||||
mutating func clearReceiptMessage() {_uniqueStorage()._receiptMessage = nil}
|
||||
|
||||
var typingMessage: SignalServiceProtos_TypingMessage {
|
||||
get {return _storage._typingMessage ?? SignalServiceProtos_TypingMessage()}
|
||||
set {_uniqueStorage()._typingMessage = newValue}
|
||||
}
|
||||
/// Returns true if `typingMessage` has been explicitly set.
|
||||
var hasTypingMessage: Bool {return _storage._typingMessage != nil}
|
||||
/// Clears the value of `typingMessage`. Subsequent reads from it will return its default value.
|
||||
mutating func clearTypingMessage() {_uniqueStorage()._typingMessage = nil}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
@ -2229,6 +2315,54 @@ extension SignalServiceProtos_Envelope.TypeEnum: SwiftProtobuf._ProtoNameProvidi
|
|||
]
|
||||
}
|
||||
|
||||
extension SignalServiceProtos_TypingMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = _protobuf_package + ".TypingMessage"
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "timestamp"),
|
||||
2: .same(proto: "action"),
|
||||
3: .same(proto: "groupId"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
switch fieldNumber {
|
||||
case 1: try decoder.decodeSingularUInt64Field(value: &self._timestamp)
|
||||
case 2: try decoder.decodeSingularEnumField(value: &self._action)
|
||||
case 3: try decoder.decodeSingularBytesField(value: &self._groupID)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._timestamp {
|
||||
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._action {
|
||||
try visitor.visitSingularEnumField(value: v, fieldNumber: 2)
|
||||
}
|
||||
if let v = self._groupID {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 3)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: SignalServiceProtos_TypingMessage, rhs: SignalServiceProtos_TypingMessage) -> Bool {
|
||||
if lhs._timestamp != rhs._timestamp {return false}
|
||||
if lhs._action != rhs._action {return false}
|
||||
if lhs._groupID != rhs._groupID {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension SignalServiceProtos_TypingMessage.Action: SwiftProtobuf._ProtoNameProviding {
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
0: .same(proto: "STARTED"),
|
||||
1: .same(proto: "STOPPED"),
|
||||
]
|
||||
}
|
||||
|
||||
extension SignalServiceProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = _protobuf_package + ".Content"
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
|
@ -2237,6 +2371,7 @@ extension SignalServiceProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._Mes
|
|||
3: .same(proto: "callMessage"),
|
||||
4: .same(proto: "nullMessage"),
|
||||
5: .same(proto: "receiptMessage"),
|
||||
6: .same(proto: "typingMessage"),
|
||||
]
|
||||
|
||||
fileprivate class _StorageClass {
|
||||
|
@ -2245,6 +2380,7 @@ extension SignalServiceProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._Mes
|
|||
var _callMessage: SignalServiceProtos_CallMessage? = nil
|
||||
var _nullMessage: SignalServiceProtos_NullMessage? = nil
|
||||
var _receiptMessage: SignalServiceProtos_ReceiptMessage? = nil
|
||||
var _typingMessage: SignalServiceProtos_TypingMessage? = nil
|
||||
|
||||
static let defaultInstance = _StorageClass()
|
||||
|
||||
|
@ -2256,6 +2392,7 @@ extension SignalServiceProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._Mes
|
|||
_callMessage = source._callMessage
|
||||
_nullMessage = source._nullMessage
|
||||
_receiptMessage = source._receiptMessage
|
||||
_typingMessage = source._typingMessage
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2276,6 +2413,7 @@ extension SignalServiceProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._Mes
|
|||
case 3: try decoder.decodeSingularMessageField(value: &_storage._callMessage)
|
||||
case 4: try decoder.decodeSingularMessageField(value: &_storage._nullMessage)
|
||||
case 5: try decoder.decodeSingularMessageField(value: &_storage._receiptMessage)
|
||||
case 6: try decoder.decodeSingularMessageField(value: &_storage._typingMessage)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
@ -2299,6 +2437,9 @@ extension SignalServiceProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._Mes
|
|||
if let v = _storage._receiptMessage {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 5)
|
||||
}
|
||||
if let v = _storage._typingMessage {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 6)
|
||||
}
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
@ -2313,6 +2454,7 @@ extension SignalServiceProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._Mes
|
|||
if _storage._callMessage != rhs_storage._callMessage {return false}
|
||||
if _storage._nullMessage != rhs_storage._nullMessage {return false}
|
||||
if _storage._receiptMessage != rhs_storage._receiptMessage {return false}
|
||||
if _storage._typingMessage != rhs_storage._typingMessage {return false}
|
||||
return true
|
||||
}
|
||||
if !storagesAreEqual {return false}
|
||||
|
|
|
@ -33,6 +33,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
@protocol OWSUDManager;
|
||||
@protocol SSKReachabilityManager;
|
||||
@protocol OWSSyncManagerProtocol;
|
||||
@protocol OWSTypingIndicators;
|
||||
|
||||
@interface SSKEnvironment : NSObject
|
||||
|
||||
|
@ -58,7 +59,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
readReceiptManager:(OWSReadReceiptManager *)readReceiptManager
|
||||
outgoingReceiptManager:(OWSOutgoingReceiptManager *)outgoingReceiptManager
|
||||
reachabilityManager:(id<SSKReachabilityManager>)reachabilityManager
|
||||
syncManager:(id<OWSSyncManagerProtocol>)syncManager NS_DESIGNATED_INITIALIZER;
|
||||
syncManager:(id<OWSSyncManagerProtocol>)syncManager
|
||||
typingIndicators:(id<OWSTypingIndicators>)typingIndicators NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
|
@ -94,6 +96,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
@property (nonatomic, readonly) OWSOutgoingReceiptManager *outgoingReceiptManager;
|
||||
@property (nonatomic, readonly) id<OWSSyncManagerProtocol> syncManager;
|
||||
@property (nonatomic, readonly) id<SSKReachabilityManager> reachabilityManager;
|
||||
@property (nonatomic, readonly) id<OWSTypingIndicators> typingIndicators;
|
||||
|
||||
// This property is configured after Environment is created.
|
||||
@property (atomic, nullable) id<OWSCallMessageHandler> callMessageHandler;
|
||||
|
|
|
@ -34,6 +34,7 @@ static SSKEnvironment *sharedSSKEnvironment;
|
|||
@property (nonatomic) OWSOutgoingReceiptManager *outgoingReceiptManager;
|
||||
@property (nonatomic) id<OWSSyncManagerProtocol> syncManager;
|
||||
@property (nonatomic) id<SSKReachabilityManager> reachabilityManager;
|
||||
@property (nonatomic) id<OWSTypingIndicators> typingIndicators;
|
||||
|
||||
@end
|
||||
|
||||
|
@ -71,6 +72,7 @@ static SSKEnvironment *sharedSSKEnvironment;
|
|||
outgoingReceiptManager:(OWSOutgoingReceiptManager *)outgoingReceiptManager
|
||||
reachabilityManager:(id<SSKReachabilityManager>)reachabilityManager
|
||||
syncManager:(id<OWSSyncManagerProtocol>)syncManager
|
||||
typingIndicators:(id<OWSTypingIndicators>)typingIndicators
|
||||
{
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
|
@ -100,6 +102,7 @@ static SSKEnvironment *sharedSSKEnvironment;
|
|||
OWSAssertDebug(outgoingReceiptManager);
|
||||
OWSAssertDebug(syncManager);
|
||||
OWSAssertDebug(reachabilityManager);
|
||||
OWSAssertDebug(typingIndicators);
|
||||
|
||||
_contactsManager = contactsManager;
|
||||
_messageSender = messageSender;
|
||||
|
@ -124,6 +127,7 @@ static SSKEnvironment *sharedSSKEnvironment;
|
|||
_outgoingReceiptManager = outgoingReceiptManager;
|
||||
_syncManager = syncManager;
|
||||
_reachabilityManager = reachabilityManager;
|
||||
_typingIndicators = typingIndicators;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
|
|
@ -8,4 +8,5 @@
|
|||
#import <SignalServiceKit/OWSOperation.h>
|
||||
#import <SignalServiceKit/OWSSyncManagerProtocol.h>
|
||||
#import <SignalServiceKit/SSKJobRecord.h>
|
||||
#import <SignalServiceKit/TSOutgoingMessage.h>
|
||||
#import <SignalServiceKit/TSYapDatabaseObject.h>
|
||||
|
|
|
@ -75,6 +75,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[[OWSOutgoingReceiptManager alloc] initWithPrimaryStorage:primaryStorage];
|
||||
id<SSKReachabilityManager> reachabilityManager = [SSKReachabilityManagerImpl new];
|
||||
id<OWSSyncManagerProtocol> syncManager = [[OWSMockSyncManager alloc] init];
|
||||
id<OWSTypingIndicators> typingIndicators = [[OWSTypingIndicatorsImpl alloc] init];
|
||||
|
||||
self = [super initWithContactsManager:contactsManager
|
||||
messageSender:messageSender
|
||||
|
@ -98,7 +99,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
readReceiptManager:readReceiptManager
|
||||
outgoingReceiptManager:outgoingReceiptManager
|
||||
reachabilityManager:reachabilityManager
|
||||
syncManager:syncManager];
|
||||
syncManager:syncManager
|
||||
typingIndicators:typingIndicators];
|
||||
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
|
363
SignalServiceKit/src/Util/TypingIndicators.swift
Normal file
363
SignalServiceKit/src/Util/TypingIndicators.swift
Normal file
|
@ -0,0 +1,363 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc(OWSTypingIndicators)
|
||||
public protocol TypingIndicators: class {
|
||||
// TODO: Use this method.
|
||||
@objc
|
||||
func didStartTypeOutgoingInput(inThread thread: TSThread)
|
||||
|
||||
// TODO: Use this method.
|
||||
@objc
|
||||
func didStopTypeOutgoingInput(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)
|
||||
|
||||
// TODO: Use this method.
|
||||
@objc
|
||||
func areTypingIndicatorsVisible(inThread thread: TSThread, recipientId: String) -> Bool
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
@objc(OWSTypingIndicatorsImpl)
|
||||
public class TypingIndicatorsImpl: NSObject, TypingIndicators {
|
||||
@objc public static let typingIndicatorStateDidChange = Notification.Name("typingIndicatorStateDidChange")
|
||||
|
||||
@objc
|
||||
public func didStartTypeOutgoingInput(inThread thread: TSThread) {
|
||||
AssertIsOnMainThread()
|
||||
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread) else {
|
||||
owsFailDebug("Could not locate outgoing indicators state")
|
||||
return
|
||||
}
|
||||
outgoingIndicators.didStartTypeOutgoingInput()
|
||||
}
|
||||
|
||||
@objc
|
||||
public func didStopTypeOutgoingInput(inThread thread: TSThread) {
|
||||
AssertIsOnMainThread()
|
||||
guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread) else {
|
||||
owsFailDebug("Could not locate outgoing indicators state")
|
||||
return
|
||||
}
|
||||
outgoingIndicators.didStopTypeOutgoingInput()
|
||||
}
|
||||
|
||||
@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()
|
||||
let incomingIndicators = ensureIncomingIndicators(forThread: thread, recipientId: recipientId, deviceId: deviceId)
|
||||
incomingIndicators.didReceiveTypingStartedMessage()
|
||||
}
|
||||
|
||||
@objc
|
||||
public func didReceiveTypingStoppedMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt) {
|
||||
AssertIsOnMainThread()
|
||||
let incomingIndicators = ensureIncomingIndicators(forThread: thread, recipientId: recipientId, deviceId: deviceId)
|
||||
incomingIndicators.didReceiveTypingStoppedMessage()
|
||||
}
|
||||
|
||||
@objc
|
||||
public func didReceiveIncomingMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt) {
|
||||
AssertIsOnMainThread()
|
||||
let incomingIndicators = ensureIncomingIndicators(forThread: thread, recipientId: recipientId, deviceId: deviceId)
|
||||
incomingIndicators.didReceiveIncomingMessage()
|
||||
}
|
||||
|
||||
@objc
|
||||
public func areTypingIndicatorsVisible(inThread thread: TSThread, recipientId: String) -> Bool {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let key = incomingIndicatorsKey(forThread: thread, recipientId: recipientId)
|
||||
guard let deviceMap = incomingIndicatorsMap[key] else {
|
||||
return false
|
||||
}
|
||||
for incomingIndicators in deviceMap.values {
|
||||
if incomingIndicators.isTyping {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
let outgoingIndicators = OutgoingIndicators(thread: thread)
|
||||
outgoingIndicatorsMap[threadId] = outgoingIndicators
|
||||
return outgoingIndicators
|
||||
}
|
||||
|
||||
// The sender maintains two timers per chat:
|
||||
//
|
||||
// A sendPause timer
|
||||
// A sendRefresh timer
|
||||
private class OutgoingIndicators {
|
||||
private let thread: TSThread
|
||||
private var sendPauseTimer: Timer?
|
||||
private var sendRefreshTimer: Timer?
|
||||
|
||||
init(thread: TSThread) {
|
||||
self.thread = thread
|
||||
}
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private var messageSender: MessageSender {
|
||||
return SSKEnvironment.shared.messageSender
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
func didStartTypeOutgoingInput() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
if sendRefreshTimer == nil {
|
||||
// If the user types a character into the compose box, and the sendRefresh timer isn’t running:
|
||||
|
||||
// Send a ACTION=TYPING message.
|
||||
sendTypingMessage(forThread: thread, action: .started)
|
||||
// Start the sendRefresh timer for 10 seconds
|
||||
sendRefreshTimer?.invalidate()
|
||||
sendRefreshTimer = Timer.weakScheduledTimer(withTimeInterval: 10,
|
||||
target: self,
|
||||
selector: #selector(OutgoingIndicators.sendRefreshTimerDidFire),
|
||||
userInfo: nil,
|
||||
repeats: false)
|
||||
// Start the sendPause timer for 5 seconds
|
||||
} else {
|
||||
// If the user types a character into the compose box, and the sendRefresh timer is running:
|
||||
|
||||
// Send nothing
|
||||
// Cancel the sendPause timer
|
||||
// Start the sendPause timer for 5 seconds again
|
||||
}
|
||||
|
||||
sendPauseTimer?.invalidate()
|
||||
sendPauseTimer = Timer.weakScheduledTimer(withTimeInterval: 5,
|
||||
target: self,
|
||||
selector: #selector(OutgoingIndicators.sendPauseTimerDidFire),
|
||||
userInfo: nil,
|
||||
repeats: false)
|
||||
}
|
||||
|
||||
func didStopTypeOutgoingInput() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// Send ACTION=STOPPED message.
|
||||
sendTypingMessage(forThread: thread, action: .stopped)
|
||||
// Cancel the sendRefresh timer
|
||||
sendRefreshTimer?.invalidate()
|
||||
sendRefreshTimer = nil
|
||||
// Cancel the sendPause timer
|
||||
sendPauseTimer?.invalidate()
|
||||
sendPauseTimer = nil
|
||||
}
|
||||
|
||||
@objc
|
||||
func sendPauseTimerDidFire() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// If the sendPause timer fires:
|
||||
|
||||
// Send ACTION=STOPPED message.
|
||||
sendTypingMessage(forThread: thread, action: .stopped)
|
||||
// Cancel the sendRefresh timer
|
||||
sendRefreshTimer?.invalidate()
|
||||
sendRefreshTimer = nil
|
||||
// Cancel the sendPause timer
|
||||
sendPauseTimer?.invalidate()
|
||||
sendPauseTimer = nil
|
||||
}
|
||||
|
||||
@objc
|
||||
func sendRefreshTimerDidFire() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// If the sendRefresh timer fires:
|
||||
|
||||
// Send ACTION=TYPING message
|
||||
sendTypingMessage(forThread: thread, action: .started)
|
||||
// Cancel the sendRefresh timer
|
||||
sendRefreshTimer?.invalidate()
|
||||
// Start the sendRefresh timer for 10 seconds again
|
||||
sendRefreshTimer = Timer.weakScheduledTimer(withTimeInterval: 10,
|
||||
target: self,
|
||||
selector: #selector(sendRefreshTimerDidFire),
|
||||
userInfo: nil,
|
||||
repeats: false)
|
||||
}
|
||||
|
||||
func didSendOutgoingMessage() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// If the user sends the message:
|
||||
|
||||
// Cancel the sendRefresh timer
|
||||
sendRefreshTimer?.invalidate()
|
||||
sendRefreshTimer = nil
|
||||
// Cancel the sendPause timer
|
||||
sendPauseTimer?.invalidate()
|
||||
sendPauseTimer = nil
|
||||
}
|
||||
|
||||
private func sendTypingMessage(forThread thread: TSThread, action: TypingIndicatorAction) {
|
||||
Logger.verbose("\(TypingIndicatorMessage.string(forTypingIndicatorAction: action))")
|
||||
|
||||
let message = TypingIndicatorMessage(thread: thread, action: action)
|
||||
messageSender.sendPromise(message: message)
|
||||
.done {
|
||||
Logger.info("Outgoing typing indicator message send succeeded.")
|
||||
}.catch { error in
|
||||
Logger.error("Outgoing typing indicator message send failed: \(error).")
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
// Map of (thread id and recipient id)-to-(device id)-to-IncomingIndicators.
|
||||
private var incomingIndicatorsMap = [String: [UInt: IncomingIndicators]]()
|
||||
|
||||
private func incomingIndicatorsKey(forThread thread: TSThread, recipientId: String) -> String {
|
||||
return "\(String(describing: thread.uniqueId)) \(recipientId)"
|
||||
}
|
||||
|
||||
private func ensureIncomingIndicators(forThread thread: TSThread, recipientId: String, deviceId: UInt) -> IncomingIndicators {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let key = incomingIndicatorsKey(forThread: thread, recipientId: recipientId)
|
||||
guard let deviceMap = incomingIndicatorsMap[key] else {
|
||||
let incomingIndicators = IncomingIndicators(recipientId: recipientId, deviceId: deviceId)
|
||||
incomingIndicatorsMap[key] = [deviceId: incomingIndicators]
|
||||
return incomingIndicators
|
||||
}
|
||||
guard let incomingIndicators = deviceMap[deviceId] else {
|
||||
let incomingIndicators = IncomingIndicators(recipientId: recipientId, deviceId: deviceId)
|
||||
var deviceMapCopy = deviceMap
|
||||
deviceMapCopy[deviceId] = incomingIndicators
|
||||
incomingIndicatorsMap[key] = deviceMapCopy
|
||||
return incomingIndicators
|
||||
}
|
||||
return incomingIndicators
|
||||
}
|
||||
|
||||
// The receiver maintains one timer for each (sender, device) in a chat:
|
||||
private class IncomingIndicators {
|
||||
private let recipientId: String
|
||||
private let deviceId: UInt
|
||||
private var displayTypingTimer: Timer?
|
||||
var isTyping = false {
|
||||
didSet {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let didChange = oldValue != isTyping
|
||||
if didChange {
|
||||
Logger.debug("isTyping changed: \(oldValue) -> \(self.isTyping)")
|
||||
|
||||
notify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(recipientId: String, deviceId: UInt) {
|
||||
self.recipientId = recipientId
|
||||
self.deviceId = deviceId
|
||||
}
|
||||
|
||||
func didReceiveTypingStartedMessage() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// If the client receives a ACTION=TYPING message:
|
||||
//
|
||||
// Cancel the displayTyping timer for that (sender, device)
|
||||
// Display the typing indicator for that (sender, device)
|
||||
// Set the displayTyping timer for 15 seconds
|
||||
displayTypingTimer?.invalidate()
|
||||
displayTypingTimer = Timer.weakScheduledTimer(withTimeInterval: 15,
|
||||
target: self,
|
||||
selector: #selector(IncomingIndicators.displayTypingTimerDidFire),
|
||||
userInfo: nil,
|
||||
repeats: false)
|
||||
isTyping = true
|
||||
}
|
||||
|
||||
func didReceiveTypingStoppedMessage() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// If the client receives a ACTION=STOPPED message:
|
||||
//
|
||||
// Cancel the displayTyping timer for that (sender, device)
|
||||
// Hide the typing indicator for that (sender, device)
|
||||
displayTypingTimer?.invalidate()
|
||||
displayTypingTimer = nil
|
||||
isTyping = false
|
||||
}
|
||||
|
||||
@objc
|
||||
func displayTypingTimerDidFire() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// If the displayTyping indicator fires:
|
||||
//
|
||||
// Cancel the displayTyping timer for that (sender, device)
|
||||
// Hide the typing indicator for that (sender, device)
|
||||
displayTypingTimer?.invalidate()
|
||||
displayTypingTimer = nil
|
||||
isTyping = false
|
||||
}
|
||||
|
||||
func didReceiveIncomingMessage() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// If the client receives a message:
|
||||
//
|
||||
// Cancel the displayTyping timer for that (sender, device)
|
||||
// Hide the typing indicator for that (sender, device)
|
||||
displayTypingTimer?.invalidate()
|
||||
displayTypingTimer = nil
|
||||
isTyping = false
|
||||
}
|
||||
|
||||
private func notify() {
|
||||
Logger.verbose("")
|
||||
NotificationCenter.default.postNotificationNameAsync(TypingIndicatorsImpl.typingIndicatorStateDidChange, object: recipientId)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue