Updated source code to be able to compile it with TigaseSwift from mix branch #siskinim-200

This commit is contained in:
Andrzej Wójcik 2020-03-13 18:45:45 +01:00
parent 65d217dcf8
commit 342bde7768
No known key found for this signature in database
GPG key ID: 2BE28BB9C1B5FF02
25 changed files with 1831 additions and 1446 deletions

View file

@ -25,7 +25,7 @@ import TigaseSwift
public class DBSchemaManager {
static let CURRENT_VERSION = 10;
static let CURRENT_VERSION = 11;
fileprivate let dbConnection: DBConnection;

15
Shared/db-schema-11.sql Normal file
View file

@ -0,0 +1,15 @@
BEGIN;
ALTER TABLE roster_items ADD COLUMN annotations TEXT;
ALTER TABLE chat_history ADD COLUMN server_msg_id TEXT;
ALTER TABLE chat_history ADD COLUMN remote_msg_id TEXT;
ALTER TABLE chat_history ADD COLUMN participant_id TEXT;
CREATE INDEX IF NOT EXISTS chat_history_account_jid_server_msg_id_idx on chat_history (
account, server_msg_id
);
COMMIT;
PRAGMA user_version = 11;

View file

@ -137,10 +137,12 @@
FEB28A9123CB5ADD00F876B7 /* WebRTC.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FEB28A8F23CB5AD600F876B7 /* WebRTC.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
FEB5EC9D1F6AE448007FE0E7 /* BaseChatViewControllerWithDataSourceContextMenuAndToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEB5EC9C1F6AE448007FE0E7 /* BaseChatViewControllerWithDataSourceContextMenuAndToolbar.swift */; };
FEB62C501DA80956001500D5 /* AvatarStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEB62C4F1DA80956001500D5 /* AvatarStore.swift */; };
FEC514221CEA3D2F003AF765 /* DBRoomsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC514211CEA3D2E003AF765 /* DBRoomsManager.swift */; };
FEC514241CEB2FF7003AF765 /* MucJoinViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC514231CEB2FF7003AF765 /* MucJoinViewController.swift */; };
FEC514261CEB74F8003AF765 /* BaseChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC514251CEB74F8003AF765 /* BaseChatViewController.swift */; };
FEC514281CEB82E9003AF765 /* MucChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC514271CEB82E9003AF765 /* MucChatViewController.swift */; };
FEC79193241AAF55007BE572 /* DBRoomStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC79192241AAF55007BE572 /* DBRoomStore.swift */; };
FEC79195241ABEF4007BE572 /* MessageState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC79194241ABEF4007BE572 /* MessageState.swift */; };
FEC79199241BE89E007BE572 /* db-schema-11.sql in Resources */ = {isa = PBXBuildFile; fileRef = FEC79198241BE89E007BE572 /* db-schema-11.sql */; };
FECEF28C23B79151007EC323 /* ChatEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECEF28B23B79151007EC323 /* ChatEntry.swift */; };
FECEF28E23B7919E007EC323 /* ChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECEF28D23B7919E007EC323 /* ChatMessage.swift */; };
FECEF29023B7926E007EC323 /* ChatAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECEF28F23B7926E007EC323 /* ChatAttachment.swift */; };
@ -160,7 +162,6 @@
FEDC6790238B05E4005C0FAB /* BlockedContactsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDC678F238B05E4005C0FAB /* BlockedContactsController.swift */; };
FEDCBF671D9C3EE700AE9129 /* RosterProviderFlat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBF661D9C3EE700AE9129 /* RosterProviderFlat.swift */; };
FEDCBF691D9C53BA00AE9129 /* RosterProviderGrouped.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDCBF681D9C53BA00AE9129 /* RosterProviderGrouped.swift */; };
FEDE3B501F7162AA00075F6E /* CustomChatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDE3B4F1F7162AA00075F6E /* CustomChatManager.swift */; };
FEDE93871D07564F00CA60A9 /* SwitchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDE93861D07564F00CA60A9 /* SwitchTableViewCell.swift */; };
FEDE93891D081C3D00CA60A9 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDE93881D081C3D00CA60A9 /* Settings.swift */; };
FEDE938C1D08AFE800CA60A9 /* VCardEditViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDE938B1D08AFE800CA60A9 /* VCardEditViewController.swift */; };
@ -389,10 +390,12 @@
FEB28A8F23CB5AD600F876B7 /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = WebRTC.framework; sourceTree = "<group>"; };
FEB5EC9C1F6AE448007FE0E7 /* BaseChatViewControllerWithDataSourceContextMenuAndToolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseChatViewControllerWithDataSourceContextMenuAndToolbar.swift; sourceTree = "<group>"; };
FEB62C4F1DA80956001500D5 /* AvatarStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarStore.swift; sourceTree = "<group>"; };
FEC514211CEA3D2E003AF765 /* DBRoomsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DBRoomsManager.swift; sourceTree = "<group>"; };
FEC514231CEB2FF7003AF765 /* MucJoinViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MucJoinViewController.swift; sourceTree = "<group>"; };
FEC514251CEB74F8003AF765 /* BaseChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseChatViewController.swift; sourceTree = "<group>"; };
FEC514271CEB82E9003AF765 /* MucChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MucChatViewController.swift; sourceTree = "<group>"; };
FEC79192241AAF55007BE572 /* DBRoomStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBRoomStore.swift; sourceTree = "<group>"; };
FEC79194241ABEF4007BE572 /* MessageState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageState.swift; sourceTree = "<group>"; };
FEC79198241BE89E007BE572 /* db-schema-11.sql */ = {isa = PBXFileReference; lastKnownFileType = text; path = "db-schema-11.sql"; sourceTree = "<group>"; };
FECEF28B23B79151007EC323 /* ChatEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatEntry.swift; sourceTree = "<group>"; };
FECEF28D23B7919E007EC323 /* ChatMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessage.swift; sourceTree = "<group>"; };
FECEF28F23B7926E007EC323 /* ChatAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAttachment.swift; sourceTree = "<group>"; };
@ -411,7 +414,6 @@
FEDC678F238B05E4005C0FAB /* BlockedContactsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactsController.swift; sourceTree = "<group>"; };
FEDCBF661D9C3EE700AE9129 /* RosterProviderFlat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RosterProviderFlat.swift; sourceTree = "<group>"; };
FEDCBF681D9C53BA00AE9129 /* RosterProviderGrouped.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RosterProviderGrouped.swift; sourceTree = "<group>"; };
FEDE3B4F1F7162AA00075F6E /* CustomChatManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomChatManager.swift; sourceTree = "<group>"; };
FEDE93861D07564F00CA60A9 /* SwitchTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchTableViewCell.swift; sourceTree = "<group>"; };
FEDE93881D081C3D00CA60A9 /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
FEDE938B1D08AFE800CA60A9 /* VCardEditViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VCardEditViewController.swift; sourceTree = "<group>"; };
@ -561,9 +563,10 @@
FE507A041CDB7B3B001A015C /* DBChatStore.swift */,
FE507A061CDB7B3B001A015C /* DBRosterStore.swift */,
FE3024311CE2036A00466497 /* DBVCardsCache.swift */,
FEC514211CEA3D2E003AF765 /* DBRoomsManager.swift */,
FE7F645A1D281B1C00B9DF56 /* DBCapabilitiesCache.swift */,
FED353882270C1D000B69C53 /* DBOMEMOStore.swift */,
FEC79192241AAF55007BE572 /* DBRoomStore.swift */,
FEC79194241ABEF4007BE572 /* MessageState.swift */,
);
path = database;
sourceTree = "<group>";
@ -697,6 +700,7 @@
FE759FF823742AC1001E78D9 /* NotificationCategory.swift */,
FECEF29723B7B838007EC323 /* db-schema-9.sql */,
FE10BCF223FD4EF000E214F3 /* db-schema-10.sql */,
FEC79198241BE89E007BE572 /* db-schema-11.sql */,
);
path = Shared;
sourceTree = "<group>";
@ -790,7 +794,6 @@
children = (
FEA8D6611F30F54B0077C12F /* XmppService.swift */,
FEA8D6631F30F9260077C12F /* XmppService_VCardExtension.swift */,
FEDE3B4F1F7162AA00075F6E /* CustomChatManager.swift */,
FE31DDE3201261A200C2AB1D /* DNSSrvDiskCache.swift */,
FE00157C2017617B00490340 /* StreamFeaturesCache.swift */,
FE1AC8F6216B8AB700D4CDAB /* NewFeaturesDetector.swift */,
@ -1025,6 +1028,7 @@
FE10BCF323FD4EF000E214F3 /* db-schema-10.sql in Resources */,
FE759FF22371F21C001E78D9 /* db-schema-6.sql in Resources */,
FE759FEF2371F21C001E78D9 /* db-schema-3.sql in Resources */,
FEC79199241BE89E007BE572 /* db-schema-11.sql in Resources */,
FE759FED2371F213001E78D9 /* db-schema-2.sql in Resources */,
FECEF29823B7B838007EC323 /* db-schema-9.sql in Resources */,
FE759FF02371F21C001E78D9 /* db-schema-4.sql in Resources */,
@ -1189,7 +1193,6 @@
FE4071E421E2605900F09B58 /* VideoCallController.swift in Sources */,
FE9E13731F260B33005C0EE5 /* StepperTableViewCell.swift in Sources */,
FE6545601E9E7B85006A14AC /* RegisterAccountController.swift in Sources */,
FEC514221CEA3D2F003AF765 /* DBRoomsManager.swift in Sources */,
FE43EB571F3DBAAE00A4CAAD /* BaseChatViewController_PreviewExtension.swift in Sources */,
FE507A181CDB7B3B001A015C /* ChatViewController.swift in Sources */,
FE137A4E21F8851D006B7F7C /* CustomTableViewController.swift in Sources */,
@ -1216,10 +1219,10 @@
FE80BDAB1D953FC4001914B0 /* SetupViewController.swift in Sources */,
FEE097621F1FCE1800B1CEAB /* TablePicketViewController.swift in Sources */,
FEDE93941D0AC01200CA60A9 /* VCardEditEmailTableViewCell.swift in Sources */,
FEDE3B501F7162AA00075F6E /* CustomChatManager.swift in Sources */,
FEDE939B1D0C38B000CA60A9 /* ContactFormTableViewCell.swift in Sources */,
FEDE93991D0C207100CA60A9 /* ContactBasicTableViewCell.swift in Sources */,
FE507A251CDB7B3B001A015C /* AccountManager.swift in Sources */,
FEC79195241ABEF4007BE572 /* MessageState.swift in Sources */,
FE719E7C22730DC3007CEEC9 /* OMEMOIdentityTableViewCell.swift in Sources */,
FEDE93901D09BB8300CA60A9 /* VCardEditPhoneTableViewCell.swift in Sources */,
FE507A221CDB7B3B001A015C /* AvatarStatusView.swift in Sources */,
@ -1244,6 +1247,7 @@
FE3024321CE2036A00466497 /* DBVCardsCache.swift in Sources */,
FE74D510234A4E1F001A925B /* ChatTableViewSystemCell.swift in Sources */,
FE507A241CDB7B3B001A015C /* GlobalSplitViewController.swift in Sources */,
FEC79193241AAF55007BE572 /* DBRoomStore.swift in Sources */,
FE233CD521E6846E00099281 /* CameraPreviewView.swift in Sources */,
FEF19F0E23479F4C005CFE9A /* ChatViewDataSource.swift in Sources */,
FE507A261CDB7B3B001A015C /* AvatarManager.swift in Sources */,

View file

@ -104,7 +104,7 @@ class BaseChatViewController: UIViewController, UITextViewDelegate, UITableViewD
super.viewWillAppear(animated);
if self.messageText?.isEmpty ?? true {
self.xmppService.dbChatStore.getMessageDraft(account: account, jid: jid) { (text) in
self.xmppService.dbChatStore.messageDraft(for: account, with: jid) { (text) in
DispatchQueue.main.async {
self.messageText = text;
}
@ -138,7 +138,7 @@ class BaseChatViewController: UIViewController, UITextViewDelegate, UITableViewD
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil);
super.viewWillDisappear(animated);
if let account = self.account, let jid = self.jid {
self.xmppService?.dbChatStore.updateMessageDraft(account: account, jid: jid, draft: messageText);
self.xmppService?.dbChatStore.storeMessage(draft: messageText, for: account, with: jid);
}
}

View file

@ -27,10 +27,10 @@ public class ChatAttachment: ChatEntry {
let url: String;
var appendix: ChatAttachmentAppendix;
init(id: Int, timestamp: Date, account: BareJID, jid: BareJID, state: MessageState, url: String, authorNickname: String?, authorJid: BareJID?, recipientNickname: String?, encryption: MessageEncryption, encryptionFingerprint: String?, appendix: ChatAttachmentAppendix, error: String?) {
init(id: Int, timestamp: Date, account: BareJID, jid: BareJID, state: MessageState, url: String, authorNickname: String?, authorJid: BareJID?, recipientNickname: String?, participantId: String?, encryption: MessageEncryption, encryptionFingerprint: String?, appendix: ChatAttachmentAppendix, error: String?) {
self.url = url;
self.appendix = appendix;
super.init(id: id, timestamp: timestamp, account: account, jid: jid, state: state, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: error);
super.init(id: id, timestamp: timestamp, account: account, jid: jid, state: state, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, participantId: participantId, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: error);
}
override public func copyText(withTimestamp: Bool, withSender: Bool) -> String? {

View file

@ -42,12 +42,15 @@ public class ChatEntry: ChatViewItemProtocol {
public let authorJid: BareJID?;
public let recipientNickname: String?;
// for MIX - id of participant
public let participantId: String?;
public let error: String?;
public let encryption: MessageEncryption;
public let encryptionFingerprint: String?;
init(id: Int, timestamp: Date, account: BareJID, jid: BareJID, state: MessageState, authorNickname: String?, authorJid: BareJID?, recipientNickname: String?, encryption: MessageEncryption, encryptionFingerprint: String?, error: String?) {
init(id: Int, timestamp: Date, account: BareJID, jid: BareJID, state: MessageState, authorNickname: String?, authorJid: BareJID?, recipientNickname: String?, participantId: String?, encryption: MessageEncryption, encryptionFingerprint: String?, error: String?) {
self.id = id;
self.timestamp = timestamp;
self.account = account;
@ -56,6 +59,7 @@ public class ChatEntry: ChatViewItemProtocol {
self.authorNickname = authorNickname;
self.authorJid = authorJid;
self.recipientNickname = recipientNickname;
self.participantId = participantId;
self.encryption = encryption;
self.encryptionFingerprint = encryptionFingerprint;
self.error = error;
@ -65,7 +69,7 @@ public class ChatEntry: ChatViewItemProtocol {
guard let item = chatItem as? ChatEntry else {
return false;
}
return self.account == item.account && self.jid == item.jid && self.state.direction == item.state.direction && self.authorNickname == item.authorNickname && self.authorJid == item.authorJid && self.recipientNickname == item.recipientNickname && abs(self.timestamp.timeIntervalSince(item.timestamp)) < allowedTimeDiff() && self.encryption == item.encryption && self.encryptionFingerprint == item.encryptionFingerprint;
return self.account == item.account && self.jid == item.jid && self.state.direction == item.state.direction && self.authorNickname == item.authorNickname && self.authorJid == item.authorJid && self.recipientNickname == item.recipientNickname && self.participantId == item.participantId && abs(self.timestamp.timeIntervalSince(item.timestamp)) < allowedTimeDiff() && self.encryption == item.encryption && self.encryptionFingerprint == item.encryptionFingerprint;
}
public func copyText(withTimestamp: Bool, withSender: Bool) -> String? {

View file

@ -26,9 +26,9 @@ class ChatLinkPreview: ChatEntry {
let url: String;
init(id: Int, timestamp: Date, account: BareJID, jid: BareJID, state: MessageState, url: String, authorNickname: String?, authorJid: BareJID?, recipientNickname: String?, encryption: MessageEncryption, encryptionFingerprint: String?, error: String?) {
init(id: Int, timestamp: Date, account: BareJID, jid: BareJID, state: MessageState, url: String, authorNickname: String?, authorJid: BareJID?, recipientNickname: String?, participantId: String?, encryption: MessageEncryption, encryptionFingerprint: String?, error: String?) {
self.url = url;
super.init(id: id, timestamp: timestamp, account: account, jid: jid, state: state, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: error);
super.init(id: id, timestamp: timestamp, account: account, jid: jid, state: state, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, participantId: participantId, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: error);
}
override func copyText(withTimestamp: Bool, withSender: Bool) -> String? {

View file

@ -26,9 +26,9 @@ class ChatMessage: ChatEntry {
let message: String;
init(id: Int, timestamp: Date, account: BareJID, jid: BareJID, state: MessageState, message: String, authorNickname: String?, authorJid: BareJID?, recipientNickname: String?, encryption: MessageEncryption, encryptionFingerprint: String?, error: String?) {
init(id: Int, timestamp: Date, account: BareJID, jid: BareJID, state: MessageState, message: String, authorNickname: String?, authorJid: BareJID?, recipientNickname: String?, participantId: String?, encryption: MessageEncryption, encryptionFingerprint: String?, error: String?) {
self.message = message;
super.init(id: id, timestamp: timestamp, account: account, jid: jid, state: state, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: error);
super.init(id: id, timestamp: timestamp, account: account, jid: jid, state: state, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, participantId: participantId, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: error);
}
override func copyText(withTimestamp: Bool, withSender: Bool) -> String? {

View file

@ -370,20 +370,23 @@ class ChatViewController : BaseChatViewControllerWithDataSourceAndContextMenuAnd
return;
}
mamModule.queryItems(with: JID(jid), start: start, queryId: "sync-2", rsm: rsmQuery ?? RSM.Query(lastItems: 100), onSuccess: {(queryid,complete,rsmResponse) in
self.log("received items from archive", queryid, complete, rsmResponse);
if rsmResponse != nil && rsmResponse!.index != 0 && rsmResponse?.first != nil {
self.syncHistory(start: start, rsm: rsmResponse?.previous(100));
} else {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) {
mamModule.queryItems(with: JID(jid), start: start, queryId: "sync-2", rsm: rsmQuery ?? RSM.Query(lastItems: 100), completionHandler: { result in
switch result {
case .success(let queryId, let complete, let rsmResponse):
self.log("received items from archive", queryId, complete, rsmResponse);
if rsmResponse != nil && rsmResponse!.index != 0 && rsmResponse?.first != nil {
self.syncHistory(start: start, rsm: rsmResponse?.previous(100));
} else {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) {
self.refreshControl.endRefreshing();
}
}
case .failure(let errorCondition, let response):
self.log("failed to retrieve items from archive", errorCondition, response);
DispatchQueue.main.async {
self.refreshControl.endRefreshing();
}
}
}, onError: {(error,stanza) in
self.log("failed to retrieve items from archive", error, stanza);
DispatchQueue.main.async {
self.refreshControl.endRefreshing();
}
});
}

View file

@ -86,15 +86,43 @@ class ChatsListViewController: CustomTableViewController {
if let item = dataSource?.item(at: indexPath) {
cell.nameLabel.textColor = Appearance.current.labelColor;
cell.nameLabel.font = item.unread > 0 ? UIFont.boldSystemFont(ofSize: cell.nameLabel.font.pointSize) : UIFont.systemFont(ofSize: cell.nameLabel.font.pointSize);
cell.lastMessageLabel.textColor = Appearance.current.secondaryLabelColor;
if item.lastMessage != nil && Settings.EnableMarkdownFormatting.getBool() {
let msg = NSMutableAttributedString(string: item.lastMessage!);
Markdown.applyStyling(attributedString: msg, font: cell.lastMessageLabel.font, showEmoticons: Settings.ShowEmoticons.getBool())
let text = NSMutableAttributedString(string: item.unread > 0 ? "" : "\u{2713}");
text.append(msg);
cell.lastMessageLabel.attributedText = text;
cell.lastMessageLabel.textColor = item.unread > 0 ? Appearance.current.labelColor : Appearance.current.secondaryLabelColor;
if let lastActivity = item.lastActivity {
switch lastActivity {
case .message(let lastMessage, let sender):
let font = item.unread > 0 ? UIFont(descriptor: cell.lastMessageLabel.font.fontDescriptor.withSymbolicTraits([.traitBold])!, size: cell.lastMessageLabel.font.fontDescriptor.pointSize) : cell.lastMessageLabel.font!;
let msg = NSMutableAttributedString(string: lastMessage);
Markdown.applyStyling(attributedString: msg, font: font, showEmoticons: Settings.ShowEmoticons.bool());
if let prefix = sender != nil ? NSMutableAttributedString(string: "\(sender!): ") : nil {
prefix.append(msg);
cell.lastMessageLabel.attributedText = prefix;
} else {
cell.lastMessageLabel.attributedText = msg;
}
case .attachment(let url, let sender):
if let fieldfont = cell.lastMessageLabel.font {
let font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits([.traitItalic, .traitBold, .traitCondensed])!, size: fieldfont.fontDescriptor.pointSize);
let msg = NSAttributedString(string: "📎 Attachment", attributes: [.font: font, .foregroundColor: cell.lastMessageLabel.textColor!.withAlphaComponent(0.8)]);
if let prefix = sender != nil ? NSMutableAttributedString(string: "\(sender!): ") : nil {
prefix.append(msg);
cell.lastMessageLabel.attributedText = prefix;
} else {
cell.lastMessageLabel.attributedText = msg;
}
} else {
let msg = NSAttributedString(string: "📎 Attachment", attributes: [.foregroundColor: cell.lastMessageLabel.textColor!.withAlphaComponent(0.8)]);
if let prefix = sender != nil ? NSMutableAttributedString(string: "\(sender!): ") : nil {
prefix.append(msg);
cell.lastMessageLabel.attributedText = prefix;
} else {
cell.lastMessageLabel.attributedText = msg;
}
}
}
} else {
cell.lastMessageLabel.text = item.lastMessage == nil ? nil : ((item.unread > 0 ? "" : "\u{2713}") + item.lastMessage!);
cell.lastMessageLabel.text = nil;
}
cell.lastMessageLabel.numberOfLines = Settings.RecentsMessageLinesNo.getInt();
// cell.lastMessageLabel.font = item.unread > 0 ? UIFont.boldSystemFont(ofSize: cell.lastMessageLabel.font.pointSize) : UIFont.systemFont(ofSize: cell.lastMessageLabel.font.pointSize);

View file

@ -169,7 +169,7 @@ class MucChatSettingsViewController: CustomTableViewController, UIImagePickerCon
self.room.modifyOptions({ (options) in
options.notifications = (item as! NotificationItem).type;
})
}, completionHandler: nil);
let account = self.room.account;
if let pushModule: SiskinPushNotificationsModule = XmppService.instance.getClient(for: account)?.modulesManager.getModule(SiskinPushNotificationsModule.ID), let pushSettings = pushModule.pushSettings {
pushModule.reenable(pushSettings: pushSettings, completionHandler: { result in

View file

@ -25,175 +25,345 @@ import Shared
import TigaseSwift
import TigaseSwiftOMEMO
open class DBChatHistoryStore: Logger {
public static let MESSAGE_NEW = Notification.Name("messengerMessageNew");
public static let MESSAGE_UPDATED = Notification.Name("messengerMessageUpdated");
public static let MESSAGE_REMOVED = Notification.Name("messageRemoved");
fileprivate static let CHAT_GET_ID_WITH_ACCOUNT_PARTICIPANT_AND_STANZA_ID = "SELECT id FROM chat_history WHERE account = :account AND jid = :jid AND stanza_id = :stanzaId";
fileprivate static let CHAT_MSG_APPEND = "INSERT INTO chat_history (account, jid, author_jid, author_nickname, recipient_nickname, timestamp, item_type, data, stanza_id, state, encryption, fingerprint, appendix) VALUES (:account, :jid, :author_jid, :author_nickname, :recipient_nickname, :timestamp, :item_type, :data, :stanza_id, :state, :encryption, :fingerprint, :appendix)";
fileprivate static let CHAT_MSGS_COUNT = "SELECT count(id) FROM chat_history WHERE account = :account AND jid = :jid";
fileprivate static let CHAT_MSGS_DELETE = "DELETE FROM chat_history WHERE account = :account AND jid = :jid";
fileprivate static let CHAT_MSGS_MARK_AS_READ = "UPDATE chat_history SET state = case state when \(MessageState.incoming_error_unread.rawValue) then \(MessageState.incoming_error.rawValue) when \(MessageState.outgoing_error_unread.rawValue) then \(MessageState.outgoing_error.rawValue) else \(MessageState.incoming.rawValue) end WHERE account = :account AND jid = :jid AND state in (\(MessageState.incoming_unread.rawValue), \(MessageState.incoming_error_unread.rawValue), \(MessageState.outgoing_error_unread.rawValue))";
fileprivate static let CHAT_MSGS_MARK_AS_READ_BEFORE = "UPDATE chat_history SET state = case state when \(MessageState.incoming_error_unread.rawValue) then \(MessageState.incoming_error.rawValue) when \(MessageState.outgoing_error_unread.rawValue) then \(MessageState.outgoing_error.rawValue) else \(MessageState.incoming.rawValue) end WHERE account = :account AND jid = :jid AND timestamp <= :before AND state in (\(MessageState.incoming_unread.rawValue), \(MessageState.incoming_error_unread.rawValue), \(MessageState.outgoing_error_unread.rawValue))";
fileprivate static let CHAT_MSG_CHANGE_STATE = "UPDATE chat_history SET state = :newState, timestamp = COALESCE(:timestamp, timestamp) WHERE id = :id AND (:oldState IS NULL OR state = :oldState)";
fileprivate static let MSG_ALREADY_ADDED = "SELECT count(id) FROM chat_history WHERE account = :account AND jid = :jid AND timestamp BETWEEN :ts_from AND :ts_to AND item_type = :item_type AND (:data IS NULL OR data = :data) AND (:stanza_id IS NULL OR (stanza_id IS NOT NULL AND stanza_id = :stanza_id)) AND (state % 2 == :direction) AND (:author_nickname is null OR author_nickname = :author_nickname)";
public class DBChatHistoryStore {
public let dbConnection:DBConnection;
fileprivate lazy var appendMessageStmt: DBStatement! = try? self.dbConnection.prepareStatement(DBChatHistoryStore.CHAT_MSG_APPEND);
fileprivate lazy var msgsCountStmt: DBStatement! = try? self.dbConnection.prepareStatement(DBChatHistoryStore.CHAT_MSGS_COUNT);
fileprivate lazy var msgsDeleteStmt: DBStatement! = try? self.dbConnection.prepareStatement(DBChatHistoryStore.CHAT_MSGS_DELETE);
fileprivate lazy var msgGetIdWithAccountPariticipantAndStanzaIdStmt: DBStatement! = try? self.dbConnection.prepareStatement(DBChatHistoryStore.CHAT_GET_ID_WITH_ACCOUNT_PARTICIPANT_AND_STANZA_ID);
fileprivate lazy var msgsMarkAsReadStmt: DBStatement! = try? self.dbConnection.prepareStatement(DBChatHistoryStore.CHAT_MSGS_MARK_AS_READ);
fileprivate lazy var msgsMarkAsReadBeforeStmt: DBStatement! = try? self.dbConnection.prepareStatement(DBChatHistoryStore.CHAT_MSGS_MARK_AS_READ_BEFORE);
fileprivate lazy var msgUpdateStateStmt: DBStatement! = try? self.dbConnection.prepareStatement(DBChatHistoryStore.CHAT_MSG_CHANGE_STATE);
open lazy var checkItemAlreadyAddedStmt: DBStatement! = try? self.dbConnection.prepareStatement(DBChatHistoryStore.MSG_ALREADY_ADDED);
fileprivate lazy var listUnreadChatsStmt: DBStatement! = try? self.dbConnection.prepareStatement("SELECT DISTINCT account, jid FROM chat_history WHERE state in (\(MessageState.incoming_unread.rawValue), \(MessageState.incoming_error_unread.rawValue), \(MessageState.outgoing_error_unread.rawValue))");
fileprivate lazy var getMessagePositionStmtInverted: DBStatement! = try? self.dbConnection.prepareStatement("SELECT count(id) FROM chat_history WHERE account = :account AND jid = :jid AND id <> :msgId AND (:showLinkPreviews OR item_type IN (\(ItemType.message.rawValue), \(ItemType.attachment.rawValue))) AND timestamp > (SELECT timestamp FROM chat_history WHERE id = :msgId)");
fileprivate lazy var markMessageAsErrorStmt: DBStatement! = try? self.dbConnection.prepareStatement("UPDATE chat_history SET state = :state, error = :error WHERE id = :id");
fileprivate lazy var getMessageErrorDetails: DBStatement! = try? self.dbConnection.prepareStatement("SELECT error FROM chat_history WHERE id = ?");
fileprivate lazy var getChatMessageWithIdStmt: DBStatement! = try! self.dbConnection.prepareStatement("SELECT id, account, jid, author_nickname, author_jid, recipient_nickname, timestamp, item_type, data, state, preview, encryption, fingerprint, error, appendix FROM chat_history WHERE id = :id");
fileprivate let getUnsentMessagesForAccountStmt: DBStatement;
fileprivate let getChatMessagesStmt: DBStatement;
fileprivate let getChatAttachmentsStmt: DBStatement;
fileprivate let updateItemStmt: DBStatement;
static let MESSAGE_NEW = Notification.Name("messageAdded");
// TODO: it looks like it is not working as expected. We should remove this notification in the future
static let MESSAGES_MARKED_AS_READ = Notification.Name("messagesMarkedAsRead");
static let MESSAGE_UPDATED = Notification.Name("messageUpdated");
static let MESSAGE_REMOVED = Notification.Name("messageRemoved");
static var instance: DBChatHistoryStore = DBChatHistoryStore.init();
fileprivate let appendMessageStmt: DBStatement = try! DBConnection.main.prepareStatement("INSERT INTO chat_history (account, jid, timestamp, item_type, data, stanza_id, state, author_nickname, author_jid, recipient_nickname, participant_id, error, encryption, fingerprint, appendix, server_msg_id, remote_msg_id) VALUES (:account, :jid, :timestamp, :item_type, :data, :stanza_id, :state, :author_nickname, :author_jid, :recipient_nickname, :participant_id, :error, :encryption, :fingerprint, :appendix, :server_msg_id, :remote_msg_id)");
// if server has MAM:2 then use server_msg_id for checking
// if there is no result, try to match using origin-id/stanza-id (if there is one in a form of UUID) and update server_msg_id if message is found
// if there is was no origin-id/stanza-id then use old check with timestamp range and all of that..
fileprivate let findItemFallback: DBStatement = try! DBConnection.main.prepareStatement("SELECT id FROM chat_history WHERE account = :account AND jid = :jid AND timestamp BETWEEN :ts_from AND :ts_to AND item_type = :item_type AND (:data IS NULL OR data = :data) AND (:stanza_id IS NULL OR (stanza_id IS NOT NULL AND stanza_id = :stanza_id)) AND (state % 2 == :direction) AND (:author_nickname is null OR author_nickname = :author_nickname) order by timestamp desc");
fileprivate let findItemByServerMsgId: DBStatement = try! DBConnection.main.prepareStatement("SELECT id FROM chat_history WHERE account = :account AND server_msg_id = :server_msg_id");
fileprivate let findItemByOriginId: DBStatement = try! DBConnection.main.prepareStatement("SELECT id FROM chat_history WHERE account = :account AND jid = :jid AND stanza_id = :stanza_id");
fileprivate let updateServerMsgId: DBStatement = try! DBConnection.main.prepareStatement("UPDATE chat_history SET server_msg_id = :server_msg_id WHERE id = :id");
fileprivate let markAsReadStmt: DBStatement = try! DBConnection.main.prepareStatement("UPDATE chat_history SET state = case state when \(MessageState.incoming_error_unread.rawValue) then \(MessageState.incoming_error.rawValue) when \(MessageState.outgoing_error_unread.rawValue) then \(MessageState.outgoing_error.rawValue) else \(MessageState.incoming.rawValue) end WHERE account = :account AND jid = :jid AND state in (\(MessageState.incoming_unread.rawValue), \(MessageState.incoming_error_unread.rawValue), \(MessageState.outgoing_error_unread.rawValue))");
fileprivate let markAsReadBeforeStmt: DBStatement = try! DBConnection.main.prepareStatement("UPDATE chat_history SET state = case state when \(MessageState.incoming_error_unread.rawValue) then \(MessageState.incoming_error.rawValue) when \(MessageState.outgoing_error_unread.rawValue) then \(MessageState.outgoing_error.rawValue) else \(MessageState.incoming.rawValue) end WHERE account = :account AND jid = :jid AND timestamp <= :before AND state in (\(MessageState.incoming_unread.rawValue), \(MessageState.incoming_error_unread.rawValue), \(MessageState.outgoing_error_unread.rawValue))");
fileprivate let markMessageAsReadStmt: DBStatement = try! DBConnection.main.prepareStatement("UPDATE chat_history SET state = case state when \(MessageState.incoming_error_unread.rawValue) then \(MessageState.incoming_error.rawValue) when \(MessageState.outgoing_error_unread.rawValue) then \(MessageState.outgoing_error.rawValue) else \(MessageState.incoming.rawValue) end WHERE id = :id AND account = :account AND jid = :jid AND state in (\(MessageState.incoming_unread.rawValue), \(MessageState.incoming_error_unread.rawValue), \(MessageState.outgoing_error_unread.rawValue))");
fileprivate let updateItemStateStmt: DBStatement = try! DBConnection.main.prepareStatement("UPDATE chat_history SET state = :newState, timestamp = COALESCE(:newTimestamp, timestamp) WHERE id = :id AND (:oldState IS NULL OR state = :oldState)");
fileprivate let updateItemStmt: DBStatement = try! DBConnection.main.prepareStatement("UPDATE chat_history SET appendix = :appendix WHERE id = :id");
fileprivate let markAsErrorStmt: DBStatement = try! DBConnection.main.prepareStatement("UPDATE chat_history SET state = :state, error = :error WHERE id = :id");
fileprivate let countItemsStmt: DBStatement = try! DBConnection.main.prepareStatement("SELECT count(id) FROM chat_history WHERE account = :account AND jid = :jid")
fileprivate let getItemIdByStanzaId: DBStatement = try! DBConnection.main.prepareStatement("SELECT id FROM chat_history WHERE account = :account AND jid = :jid AND stanza_id = :stanza_id ORDER BY timestamp DESC");
fileprivate let getChatMessagesStmt: DBStatement = try! DBConnection.main.prepareStatement("SELECT id, author_nickname, author_jid, recipient_nickname, participant_id, timestamp, item_type, data, state, preview, encryption, fingerprint, error, appendix FROM chat_history WHERE account = :account AND jid = :jid AND (:showLinkPreviews OR item_type IN (\(ItemType.message.rawValue), \(ItemType.attachment.rawValue))) ORDER BY timestamp DESC LIMIT :limit OFFSET :offset");
fileprivate let getChatMessageWithIdStmt: DBStatement = try! DBConnection.main.prepareStatement("SELECT id, account, jid, author_nickname, author_jid, recipient_nickname, participant_id, timestamp, item_type, data, state, preview, encryption, fingerprint, error, appendix FROM chat_history WHERE id = :id");
fileprivate let getChatAttachmentsStmt: DBStatement = try! DBConnection.main.prepareStatement("SELECT id, author_nickname, author_jid, recipient_nickname, participant_id, timestamp, item_type, data, state, preview, encryption, fingerprint, error, appendix FROM chat_history WHERE account = :account AND jid = :jid AND item_type = \(ItemType.attachment.rawValue) ORDER BY timestamp DESC");
fileprivate let getChatMessagePosition: DBStatement = try! DBConnection.main.prepareStatement("SELECT count(id) FROM chat_history WHERE account = :account AND jid = :jid AND id <> :msgId AND (:showLinkPreviews OR item_type IN (\(ItemType.message.rawValue), \(ItemType.attachment.rawValue))) AND timestamp > (SELECT timestamp FROM chat_history WHERE id = :msgId)")
fileprivate let removeChatHistoryStmt: DBStatement = try! DBConnection.main.prepareStatement("DELETE FROM chat_history WHERE account = :account AND (:jid IS NULL OR jid = :jid)");
// fileprivate let searchHistoryStmt: DBStatement = try! DBConnection.main.prepareStatement("SELECT chat_history.id as id, chat_history.account as account, chat_history.jid as jid, author_nickname, author_jid, participant_id, chat_history.timestamp as timestamp, item_type, chat_history.data as data, state, preview, chat_history.encryption as encryption, fingerprint FROM chat_history INNER JOIN chat_history_fts_index ON chat_history.id = chat_history_fts_index.rowid LEFT JOIN chats ON chats.account = chat_history.account AND chats.jid = chat_history.jid WHERE (chats.id IS NOT NULL OR chat_history.author_nickname is NULL) AND chat_history_fts_index MATCH :query AND (:account IS NULL OR chat_history.account = :account) AND (:jid IS NULL OR chat_history.jid = :jid) AND item_type = \(ItemType.message.rawValue) ORDER BY chat_history.timestamp DESC")
fileprivate let getUnsentMessagesForAccountStmt: DBStatement = try! DBConnection.main.prepareStatement("SELECT ch.account as account, ch.jid as jid, ch.item_type as item_type, ch.data as data, ch.stanza_id as stanza_id, ch.encryption as encryption FROM chat_history ch WHERE ch.account = :account AND ch.state = \(MessageState.outgoing_unsent.rawValue) ORDER BY timestamp ASC");
fileprivate lazy var countUnsentMessagesStmt: DBStatement! = try! DBConnection.main.prepareStatement("SELECT count(id) FROM chat_history WHERE state = \(MessageState.outgoing_unsent.rawValue)");
fileprivate let removeItemStmt: DBStatement = try! DBConnection.main.prepareStatement("DELETE FROM chat_history WHERE id = :id");
fileprivate lazy var removeItemStmt: DBStatement! = try! self.dbConnection.prepareStatement("DELETE FROM chat_history WHERE id = :id");
fileprivate lazy var countUnsentMessagesStmt: DBStatement! = try! self.dbConnection.prepareStatement("SELECT count(id) FROM chat_history WHERE state = \(MessageState.outgoing_unsent.rawValue)");
fileprivate let dispatcher: QueueDispatcher;
public static let instance = DBChatHistoryStore(dbConnection: DBConnection.main);
public init(dbConnection:DBConnection) {
self.dispatcher = QueueDispatcher(label: "chat_history_store");
self.dbConnection = dbConnection;
self.getUnsentMessagesForAccountStmt = try! self.dbConnection.prepareStatement("SELECT ch.account as account, ch.jid as jid, ch.data as data, ch.stanza_id as stanza_id, ch.encryption as encryption FROM chat_history ch WHERE ch.account = :account AND ch.state = \(MessageState.outgoing_unsent.rawValue) ORDER BY timestamp ASC");
self.getChatMessagesStmt = try! dbConnection.prepareStatement("SELECT id, author_nickname, author_jid, recipient_nickname, timestamp, item_type, data, state, preview, encryption, fingerprint, error, appendix FROM chat_history WHERE account = :account AND jid = :jid AND (:showLinkPreviews OR item_type IN (\(ItemType.message.rawValue), \(ItemType.attachment.rawValue))) ORDER BY timestamp DESC LIMIT :limit OFFSET :offset");
self.getChatAttachmentsStmt = try! dbConnection.prepareStatement("SELECT id, author_nickname, author_jid, recipient_nickname, timestamp, item_type, data, state, preview, encryption, fingerprint, error, appendix FROM chat_history WHERE account = :account AND jid = :jid AND item_type = \(ItemType.attachment.rawValue) ORDER BY timestamp DESC");
self.updateItemStmt = try! dbConnection.prepareStatement("UPDATE chat_history SET appendix = :appendix WHERE id = :id")
super.init();
NotificationCenter.default.addObserver(self, selector: #selector(DBChatHistoryStore.accountRemoved), name: NSNotification.Name(rawValue: "accountRemoved"), object: nil);
static func convertToAttachments() {
let diskCacheUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent(Bundle.main.bundleIdentifier!).appendingPathComponent("download", isDirectory: true);
guard FileManager.default.fileExists(atPath: diskCacheUrl.path) else {
return;
}
let previewsToConvert = try! DBConnection.main.prepareStatement("SELECT id FROM chat_history WHERE preview IS NOT NULL").query(map: { cursor -> Int in
return cursor["id"]!;
});
let convertStmt: DBStatement = try! DBConnection.main.prepareStatement("SELECT id, account, jid, author_nickname, author_jid, timestamp, item_type, data, state, preview, encryption, fingerprint, error, appendix, preview, stanza_id FROM chat_history WHERE id = ?");
let removePreviewStmt: DBStatement = try! DBConnection.main.prepareStatement("UPDATE chat_history SET preview = NULL WHERE id = ?");
previewsToConvert.forEach { id in
guard let (item, previews, stanzaId) = try! convertStmt.findFirst(id, map: { (cursor) -> (ChatMessage, [String:String], String?)? in
let account: BareJID = cursor["account"]!;
let jid: BareJID = cursor["jid"]!;
let stanzaId: String? = cursor["stanza_id"];
guard let item = DBChatHistoryStore.instance.itemFrom(cursor: cursor, for: account, with: jid) as? ChatMessage, let previewStr: String = cursor["preview"] else {
return nil;
}
var previews: [String:String] = [:];
previewStr.split(separator: "\n").forEach { (line) in
let tmp = line.split(separator: "\t").map({String($0)});
if (!tmp[1].starts(with: "ERROR")) && (tmp[1] != "NONE") {
previews[tmp[0]] = tmp[1];
}
}
return (item, previews, stanzaId);
}) else {
return;
}
if previews.isEmpty {
_ = try! removePreviewStmt.update(item.id);
} else {
print("converting for:", item.account, "with:", item.jid, "previews:", previews);
if previews.count == 1 {
let isAttachmentOnly = URL(string: item.message) != nil;
if isAttachmentOnly {
let appendix = ChatAttachmentAppendix();
DBChatHistoryStore.instance.appendItem(for: item.account, with: item.jid, state: item.state, authorNickname: item.authorNickname, authorJid: item.authorJid, recipientNickname: nil, participantId: nil, type: .attachment, timestamp: item.timestamp, stanzaId: stanzaId, serverMsgId: nil, remoteMsgId: nil, data: item.message, encryption: item.encryption, encryptionFingerprint: item.encryptionFingerprint, chatAttachmentAppendix: appendix, linkPreviewAction: .none, completionHandler: { newId in
DBChatHistoryStore.instance.remove(item: item);
});
} else {
if #available(iOS 13.0, *) {
DBChatHistoryStore.instance.appendItem(for: item.account, with: item.jid, state: item.state, authorNickname: item.authorNickname, authorJid: item.authorJid, recipientNickname: nil, participantId: nil, type: .linkPreview, timestamp: item.timestamp, stanzaId: stanzaId, serverMsgId: nil, remoteMsgId: nil, data: previews.keys.first ?? item.message, encryption: item.encryption, encryptionFingerprint: item.encryptionFingerprint, linkPreviewAction: .none, completionHandler: { newId in
_ = try! removePreviewStmt.update(item.id);
});
} else {
_ = try! removePreviewStmt.update(item.id);
}
}
} else {
if #available(iOS 13.0, *) {
let group = DispatchGroup();
group.enter();
group.notify(queue: DispatchQueue.main, execute: {
_ = try! removePreviewStmt.update(item.id);
})
for (url, previewId) in previews {
group.enter();
DBChatHistoryStore.instance.appendItem(for: item.account, with: item.jid, state: item.state, authorNickname: item.authorNickname, authorJid: item.authorJid, recipientNickname: nil, participantId: nil, type: .linkPreview, timestamp: item.timestamp, stanzaId: stanzaId, serverMsgId: nil, remoteMsgId: nil, data: url, encryption: item.encryption, encryptionFingerprint: item.encryptionFingerprint, linkPreviewAction: .none, completionHandler: { newId in
group.leave();
});
}
group.leave();
} else {
_ = try! removePreviewStmt.update(item.id);
}
}
}
}
try? FileManager.default.removeItem(at: diskCacheUrl);
}
public func appendItem(for account: BareJID, with jid: BareJID, state inState: MessageState, authorNickname: String? = nil, authorJid: BareJID? = nil, recipientNickname: String? = nil, type: ItemType = .message, timestamp inTimestamp: Date, stanzaId id: String?, data: String, chatState: ChatState? = nil, errorCondition: ErrorCondition? = nil, errorMessage: String? = nil, encryption: MessageEncryption, encryptionFingerprint: String?, chatAttachmentAppendix: ChatAttachmentAppendix? = nil, skipItemAlreadyExists: Bool = false, completionHandler: ((Int)->Void)?) {
public init() {
dispatcher = QueueDispatcher(label: "chat_history_store");
}
open func process(chatState: ChatState, for account: BareJID, with jid: BareJID) {
dispatcher.async {
let timestamp = Date(timeIntervalSince1970: Double(Int64(inTimestamp.timeIntervalSince1970 * 1000)) / 1000);
guard !inState.isError || id == nil || !self.processOutgoingError(for: account, with: jid, stanzaId: id!, errorCondition: errorCondition, errorMessage: errorMessage) else {
DBChatStore.instance.process(chatState: chatState, for: account, with: jid);
}
}
public enum MessageSource {
case stream
case archive(source: BareJID, version: MessageArchiveManagementModule.Version, messageId: String, timestamp: Date)
case carbons(action: MessageCarbonsModule.Action)
}
open func append(for account: BareJID, message: Message, source: MessageSource) {
let direction: MessageDirection = account == message.from?.bareJid ? .outgoing : .incoming;
guard let jidFull = direction == .outgoing ? message.to : message.from else {
// sender jid should always be there..
return;
}
let jid = jidFull.bareJid;
let (decryptedBody, encryption, fingerprint) = MessageEventHandler.prepareBody(message: message, forAccount: account);
guard let body = decryptedBody else {
// only if carbon!!
switch source {
case .carbons(let action):
if action == .received {
if (message.type ?? .normal) != .error, let chatState = message.chatState, message.delay == nil {
DBChatHistoryStore.instance.process(chatState: chatState, for: account, with: jid);
}
}
default:
if (message.type ?? .normal) != .error, let chatState = message.chatState, message.delay == nil {
DBChatHistoryStore.instance.process(chatState: chatState, for: account, with: jid);
}
break;
}
return;
}
let itemType = MessageEventHandler.itemType(fromMessage: message);
let stanzaId = message.originId ?? message.id;
var stableIds = message.stanzaId;
var fromArchive = false;
var inTimestamp: Date?;
switch source {
case .archive(let source, let version, let messageId, let timestamp):
if version == .MAM2 {
if stableIds == nil {
stableIds = [source: messageId];
} else {
stableIds?[source] = messageId;
}
}
inTimestamp = timestamp;
fromArchive = true;
default:
inTimestamp = message.delay?.stamp;
break;
}
let serverMsgId: String? = stableIds?[account];
let remoteMsgId: String? = stableIds?[jid];
var originId: String?;
if let tmp = message.originId, UUID(uuidString: tmp) != nil {
originId = tmp;
}
let (authorNickname, authorJid, recipientNickname, participantId) = MessageEventHandler.extractRealAuthor(from: message, for: account, with: jidFull);
let state = MessageEventHandler.calculateState(direction: MessageEventHandler.calculateDirection(direction: direction, for: account, with: jid, authorNickname: authorNickname, authorJid: authorJid), isError: (message.type ?? .chat) == .error, isUnread: !fromArchive);
dispatcher.async {
let timestamp = Date(timeIntervalSince1970: Double(Int64((inTimestamp ?? Date()).timeIntervalSince1970 * 1000)) / 1000);
guard !state.isError || stanzaId == nil || !self.processOutgoingError(for: account, with: jid, stanzaId: stanzaId!, errorCondition: message.errorCondition, errorMessage: message.errorText) else {
return;
}
if let id = self.findItemId(for: account, with: jid, serverMsgId: serverMsgId, originId: originId, timestamp: timestamp, direction: state.direction, itemType: itemType, stanzaId: message.id, authorNickname: authorNickname, data: body) {
// this message was already added to the store..
// should this be here...?
if let chatState = message.chatState {
DBChatStore.instance.process(chatState: chatState, for: account, with: jid);
}
return;
}
guard skipItemAlreadyExists || !self.checkItemAlreadyAdded(for: account, with: jid, authorNickname: authorNickname, type: type, timestamp: timestamp, direction: inState.direction, stanzaId: id, data: data) else {
return;
self.appendItemSync(for: account, with: jid, state: state, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, participantId: participantId, type: itemType, timestamp: timestamp, stanzaId: stanzaId, serverMsgId: serverMsgId, remoteMsgId: remoteMsgId, data: body, chatState: message.chatState, errorCondition: message.errorCondition, errorMessage: message.errorText, encryption: encryption, encryptionFingerprint: fingerprint, chatAttachmentAppendix: nil, linkPreviewAction: .auto, completionHandler: nil);
}
}
public enum LinkPreviewAction {
case auto
case none
case only
}
private func findItemId(for account: BareJID, with jid: BareJID, serverMsgId: String?, originId: String?, timestamp: Date, direction: MessageDirection, itemType: ItemType, stanzaId: String?, authorNickname: String?, data: String?) -> Int? {
if serverMsgId != nil {
if let id = try! self.findItemByServerMsgId.findFirst(["server_msg_id": serverMsgId, "account": account] as [String: Any?], map: { cursor -> Int? in
return cursor["id"];
}) {
return id;
}
let state = self.calculateState(for: account, with: jid, timestamp: timestamp, state: inState);
}
var id: Int?;
if originId != nil {
id = try! self.findItemByOriginId.findFirst(["stanza_id": originId, "account": account, "jid": jid] as [String: Any?], map: { cursor -> Int? in
return cursor["id"];
})
}
if id == nil {
let range = stanzaId == nil ? 5.0 : 60.0;
let ts_from = timestamp.addingTimeInterval(-60 * range);
let ts_to = timestamp.addingTimeInterval(60 * range);
let params: [String: Any?] = ["account": account, "jid": jid, "ts_from": ts_from, "ts_to": ts_to, "item_type": itemType.rawValue, "direction": direction.rawValue, "stanza_id": stanzaId, "data": data, "author_nickname": authorNickname];
id = try! self.findItemFallback.findFirst(params, map: { cursor -> Int? in
return cursor["id"];
})
}
if id != nil && serverMsgId != nil {
_ = try! self.updateServerMsgId.update(["id": id, "server_msg_id": serverMsgId] as [String: Any?]);
}
return id;
}
private func appendItemSync(for account: BareJID, with jid: BareJID, state: MessageState, authorNickname: String?, authorJid: BareJID?, recipientNickname: String?, participantId: String?, type: ItemType, timestamp: Date, stanzaId: String?, serverMsgId: String?, remoteMsgId: String?, data: String, chatState: ChatState?, errorCondition: ErrorCondition?, errorMessage: String? , encryption: MessageEncryption, encryptionFingerprint: String?, chatAttachmentAppendix: ChatAttachmentAppendix?, linkPreviewAction: LinkPreviewAction, completionHandler: ((Int) -> Void)?) {
if linkPreviewAction != .only {
var appendix: String? = nil;
if let attachmentAppendix = chatAttachmentAppendix {
if let appendixData = try? JSONEncoder().encode(attachmentAppendix) {
appendix = String(data: appendixData, encoding: .utf8);
}
}
let params:[String:Any?] = ["account" : account, "jid" : jid, "timestamp": timestamp, "data": data, "item_type": type.rawValue, "state": state.rawValue, "stanza_id": id, "author_jid" : authorJid, "author_nickname": authorNickname,
"recipient_nickname": recipientNickname, "encryption": encryption.rawValue, "fingerprint": encryptionFingerprint, "appendix": appendix]
let params: [String:Any?] = ["account": account, "jid": jid, "timestamp": timestamp, "data": data, "item_type": type.rawValue, "state": state.rawValue, "stanza_id": stanzaId, "author_nickname": authorNickname, "author_jid": authorJid, "recipient_nickname": recipientNickname, "participant_id": participantId, "encryption": encryption.rawValue, "fingerprint": encryptionFingerprint, "error": state.isError ? (errorMessage ?? errorCondition?.rawValue ?? "Unknown error") : nil, "appendix": appendix, "server_msg_id": serverMsgId, "remote_msg_id": remoteMsgId];
guard let msgId = try! self.appendMessageStmt.insert(params) else {
return;
}
completionHandler?(msgId);
var item: ChatViewItemProtocol?;
switch type {
case .message:
item = ChatMessage(id: msgId, timestamp: timestamp, account: account, jid: jid, state: state, message: data, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: errorMessage);
item = ChatMessage(id: msgId, timestamp: timestamp, account: account, jid: jid, state: state, message: data, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, participantId: participantId, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: errorMessage);
case .attachment:
item = ChatAttachment(id: msgId, timestamp: timestamp, account: account, jid: jid, state: state, url: data, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, encryption: encryption, encryptionFingerprint: encryptionFingerprint, appendix: chatAttachmentAppendix ?? ChatAttachmentAppendix(), error: errorMessage);
item = ChatAttachment(id: msgId, timestamp: timestamp, account: account, jid: jid, state: state, url: data, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, participantId: participantId, encryption: encryption, encryptionFingerprint: encryptionFingerprint, appendix: chatAttachmentAppendix ?? ChatAttachmentAppendix(), error: errorMessage);
case .linkPreview:
if #available(iOS 13.0, *), Settings.linkPreviews.bool() {
item = ChatLinkPreview(id: msgId, timestamp: timestamp, account: account, jid: jid, state: state, url: data, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: errorMessage);
item = ChatLinkPreview(id: msgId, timestamp: timestamp, account: account, jid: jid, state: state, url: data, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, participantId: participantId, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: errorMessage);
}
}
if item != nil {
DBChatStore.instance.newMessage(for: account, with: jid, timestamp: timestamp, message: encryption.message() ?? data, state: state, remoteChatState: state.direction == .incoming ? chatState : nil) {
DBChatStore.instance.newMessage(for: account, with: jid, timestamp: timestamp, itemType: type, message: encryption.message() ?? data, state: state, remoteChatState: state.direction == .incoming ? chatState : nil, senderNickname: authorNickname) {
NotificationCenter.default.post(name: DBChatHistoryStore.MESSAGE_NEW, object: item);
}
}
}
if linkPreviewAction != .none && type == .message, #available(iOS 13.0, *) {
// if we may have previews, we should add them here..
if let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue | NSTextCheckingResult.CheckingType.address.rawValue) {
let matches = detector.matches(in: data, range: NSMakeRange(0, data.utf16.count));
matches.forEach { match in
if let url = match.url, let scheme = url.scheme, ["https", "http"].contains(scheme) {
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: state, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, participantId: participantId, type: .linkPreview, timestamp: timestamp, stanzaId: nil, serverMsgId: nil, remoteMsgId: nil, data: url.absoluteString, encryption: .none, encryptionFingerprint: nil, linkPreviewAction: .none, completionHandler: nil);
}
if let address = match.components {
let query = address.values.joined(separator: ",").addingPercentEncoding(withAllowedCharacters: .urlHostAllowed);
let mapUrl = URL(string: "http://maps.apple.com/?q=\(query!)")!;
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: state, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, participantId: participantId, type: .linkPreview, timestamp: timestamp, stanzaId: nil, serverMsgId: nil, remoteMsgId: nil, data: mapUrl.absoluteString, encryption: .none, encryptionFingerprint: nil, linkPreviewAction: .none, completionHandler: nil);
}
}
}
}
}
private func calculateState(for account: BareJID, with jid: BareJID, timestamp: Date, state: MessageState) -> MessageState {
guard state.isUnread else {
return state;
open func appendItem(for account: BareJID, with jid: BareJID, state: MessageState, authorNickname: String?, authorJid: BareJID?, recipientNickname: String?, participantId: String?, type: ItemType, timestamp: Date, stanzaId: String?, serverMsgId: String?, remoteMsgId: String?, data: String, chatState: ChatState? = nil, errorCondition: ErrorCondition? = nil, errorMessage: String? = nil, encryption: MessageEncryption, encryptionFingerprint: String?, chatAttachmentAppendix: ChatAttachmentAppendix? = nil, linkPreviewAction: LinkPreviewAction, completionHandler: ((Int) -> Void)?) {
dispatcher.async {
self.appendItemSync(for: account, with: jid, state: state, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, participantId: participantId, type: type, timestamp: timestamp, stanzaId: stanzaId, serverMsgId: serverMsgId, remoteMsgId: remoteMsgId, data: data, chatState: chatState, errorCondition: errorCondition, errorMessage: errorMessage, encryption: encryption, encryptionFingerprint: encryptionFingerprint, chatAttachmentAppendix: chatAttachmentAppendix, linkPreviewAction: linkPreviewAction, completionHandler: completionHandler);
}
let readTill = DBChatStore.instance.getChat(for: account, with: jid)?.readTill ?? Date.distantPast;
guard timestamp <= readTill else {
return state;
}
return state.toRead();
}
fileprivate func processOutgoingError(for account: BareJID, with jid: BareJID, stanzaId: String, errorCondition: ErrorCondition?, errorMessage: String?) -> Bool {
guard let itemId = self.getMessageIdInt(account: account, jid: jid, stanzaId: stanzaId) else {
open func removeHistory(for account: BareJID, with jid: BareJID?) {
dispatcher.async {
let params: [String: Any?] = ["account": account, "jid": jid];
_ = try! self.removeChatHistoryStmt.update(params);
}
}
fileprivate func processOutgoingError(for account: BareJID, with jid: BareJID, stanzaId: String, errorCondition: ErrorCondition?, errorMessage: String?) -> Bool {
guard let itemId = DBChatHistoryStore.instance.getItemId(for: account, with: jid, stanzaId: stanzaId) else {
return false;
}
let params: [String: Any?] = ["id": itemId, "state": MessageState.outgoing_error_unread.rawValue, "error": errorMessage ?? errorCondition?.rawValue ?? "Unknown error"];
guard try! self.markMessageAsErrorStmt.update(params) > 0 else {
guard try! self.markAsErrorStmt.update(params) > 0 else {
return false;
}
DBChatStore.instance.newMessage(for: account, with: jid, timestamp: Date(timeIntervalSince1970: 0), message: nil, state: .outgoing_error_unread) {
DBChatStore.instance.newMessage(for: account, with: jid, timestamp: Date(timeIntervalSince1970: 0), itemType: nil, message: nil, state: .outgoing_error_unread) {
self.itemUpdated(withId: itemId, for: account, with: jid);
}
return true;
}
open func history(for account: BareJID, jid: BareJID, before: Int? = nil, limit: Int, completionHandler: @escaping (([ChatViewItemProtocol]) -> Void)) {
dispatcher.async {
// let count = try! self.msgsCountStmt.scalar(["account": account, "jid": jid] as [String : Any?]) ?? 0;
if before != nil {
let params: [String: Any?] = ["account": account, "jid": jid, "msgId": before!, "showLinkPreviews": self.linkPreviews];
let offset = try! self.getMessagePositionStmtInverted.scalar(params)!;
completionHandler( self.history(for: account, jid: jid, offset: offset, limit: limit));
} else {
completionHandler(self.history(for: account, jid: jid, offset: 0, limit: limit));
}
}
}
open func history(for account: BareJID, jid: BareJID, before: Int? = nil, limit: Int) -> [ChatViewItemProtocol] {
return dispatcher.sync {
if before != nil {
let offset = try! getMessagePositionStmtInverted.scalar(["account": account, "jid": jid, "msgId": before!, "showLinkPreviews": self.linkPreviews])!;
return history(for: account, jid: jid, offset: offset, limit: limit);
} else {
return history(for: account, jid: jid, offset: 0, limit: limit);
}
}
}
fileprivate func history(for account: BareJID, jid: BareJID, offset: Int, limit: Int) -> [ChatViewItemProtocol] {
let params: [String: Any?] = ["account": account, "jid": jid, "offset": offset, "limit": limit, "showLinkPreviews": linkPreviews];
return try! getChatMessagesStmt.query(params) { (cursor) -> ChatViewItemProtocol? in
return itemFrom(cursor: cursor, for: account, with: jid);
}
}
open func checkItemAlreadyAdded(for account: BareJID, with jid: BareJID, authorNickname: String? = nil, type: ItemType, timestamp: Date, direction: MessageDirection, stanzaId: String?, data: String?) -> Bool {
let range = stanzaId == nil ? 5.0 : 60.0;
let ts_from = timestamp.addingTimeInterval(-60 * range);
let ts_to = timestamp.addingTimeInterval(60 * range);
let params: [String: Any?] = ["account": account, "jid": jid, "ts_from": ts_from, "ts_to": ts_to, "item_type": type.rawValue, "direction": direction.rawValue, "stanza_id": stanzaId, "data": data, "author_nickname": authorNickname];
return (try! checkItemAlreadyAddedStmt.scalar(params) ?? 0) > 0;
}
open func countUnsentMessages(completionHandler: @escaping (Int)->Void) {
dispatcher.async {
let result = try! self.countUnsentMessagesStmt.scalar() ?? 0;
@ -201,39 +371,86 @@ open class DBChatHistoryStore: Logger {
}
}
open func forEachUnreadChat(forEach: (_ account: BareJID, _ jid: BareJID)->Void) {
try! listUnreadChatsStmt.query(forEach: { (cursor) -> Void in
let account: BareJID = cursor["account"]!;
let jid: BareJID = cursor["jid"]!;
forEach(account, jid);
});
}
open func markOutgoingAsError(for account: BareJID, with jid: BareJID, stanzaId: String, errorCondition: ErrorCondition?, errorMessage: String?) {
dispatcher.async {
_ = self.processOutgoingError(for: account, with: jid, stanzaId: stanzaId, errorCondition: errorCondition, errorMessage: errorMessage);
}
}
open func markAsRead(for account: BareJID, with jid: BareJID, before: Date, completionHandler: (()->Void)? = nil) {
dispatcher.async {
// if before == nil {
// let params:[String:Any?] = ["account":account, "jid":jid];
// let updatedRecords = try! self.msgsMarkAsReadStmt.update(params);
// if updatedRecords > 0 {
// DBChatStore.instance.markAsRead(for: account, with: jid, completionHandler: completionHandler);
// } else {
// completionHandler?();
// }
// } else {
let params:[String:Any?] = ["account":account, "jid":jid, "before": before];
let updatedRecords = try! self.msgsMarkAsReadBeforeStmt.update(params);
DBChatStore.instance.markAsRead(for: account, with: jid, before: before, count: updatedRecords, completionHandler: completionHandler);
open func markAsRead(for account: BareJID, with jid: BareJID, messageId: String? = nil) {
dispatcher.async {
if let id = messageId {
var params: [String: Any?] = ["account": account, "jid": jid, "stanza_id": id];
if let msgId = try! self.getItemIdByStanzaId.scalar(params) {
params = ["account": account, "jid": jid, "id": msgId];
let updateRecords = try! self.markMessageAsReadStmt.update(params);
if updateRecords > 0 {
DBChatStore.instance.markAsRead(for: account, with: jid, count: 1);
DispatchQueue.main.async {
NotificationCenter.default.post(name: DBChatHistoryStore.MESSAGES_MARKED_AS_READ, object: self, userInfo: ["account": account, "jid": jid]);
}
}
}
} else {
let params: [String: Any?] = ["account": account, "jid": jid];
let updateRecords = try! self.markAsReadStmt.update(params);
if updateRecords > 0 {
DBChatStore.instance.markAsRead(for: account, with: jid);
DispatchQueue.main.async {
NotificationCenter.default.post(name: DBChatHistoryStore.MESSAGES_MARKED_AS_READ, object: self, userInfo: ["account": account, "jid": jid]);
}
}
}
}
}
open func markAsRead(for account: BareJID, with jid: BareJID, before: Date, completionHandler: (()->Void)? = nil) {
dispatcher.async {
let params: [String: Any?] = ["account": account, "jid": jid, "before": before];
let updateRecords = try! self.markAsReadBeforeStmt.update(params);
if updateRecords > 0 {
DBChatStore.instance.markAsRead(for: account, with: jid, count: updateRecords, completionHandler: completionHandler);
DispatchQueue.main.async {
NotificationCenter.default.post(name: DBChatHistoryStore.MESSAGES_MARKED_AS_READ, object: self, userInfo: ["account": account, "jid": jid]);
}
}
}
}
open func getItemId(for account: BareJID, with jid: BareJID, stanzaId: String) -> Int? {
return dispatcher.sync {
let params: [String: Any?] = ["account": account, "jid": jid, "stanza_id": stanzaId];
return try! self.getItemIdByStanzaId.scalar(params);
}
}
open func itemPosition(for account: BareJID, with jid: BareJID, msgId: Int) -> Int? {
return dispatcher.sync {
let params: [String: Any?] = ["account": account, "jid": jid, "msgId": msgId, "showLinkPreviews": linkPreviews];
return try! self.getChatMessagePosition.scalar(params);
}
}
open func updateItemState(for account: BareJID, with jid: BareJID, stanzaId: String, from oldState: MessageState, to newState: MessageState, withTimestamp timestamp: Date? = nil) {
dispatcher.async {
guard let msgId = self.getItemId(for: account, with: jid, stanzaId: stanzaId) else {
return;
}
self.updateItemState(for: account, with: jid, itemId: msgId, from: oldState, to: newState, withTimestamp: timestamp);
}
}
open func updateItemState(for account: BareJID, with jid: BareJID, itemId msgId: Int, from oldState: MessageState, to newState: MessageState, withTimestamp timestamp: Date?) {
dispatcher.async {
let params: [String: Any?] = ["id": msgId, "oldState": oldState.rawValue, "newState": newState.rawValue, "newTimestamp": timestamp];
guard (try! self.updateItemStateStmt.update(params)) > 0 else {
return;
}
self.itemUpdated(withId: msgId, for: account, with: jid);
}
}
fileprivate var findLinkPreviewsForMessageStmt: DBStatement?;
open func remove(item: ChatViewItemProtocol) {
@ -246,7 +463,7 @@ open class DBChatHistoryStore: Logger {
if #available(iOS 13.0, *), let item = item as? ChatMessage {
if self.findLinkPreviewsForMessageStmt == nil {
self.findLinkPreviewsForMessageStmt = try! self.dbConnection.prepareStatement("SELECT id, data FROM chat_history WHERE account = :account AND jid = :jid AND timestamp = :timestamp AND item_type = \(ItemType.linkPreview.rawValue) AND id > :afterId");
self.findLinkPreviewsForMessageStmt = try! DBConnection.main.prepareStatement("SELECT id, data FROM chat_history WHERE account = :account AND jid = :jid AND timestamp = :timestamp AND item_type = \(ItemType.linkPreview.rawValue) AND id > :afterId");
}
// for chat message we might have a link previews which we need to remove..
let linkParams: [String: Any?] = ["account": item.account, "jid": item.jid, "timestamp": item.timestamp, "afterId": item.id];
@ -270,41 +487,6 @@ open class DBChatHistoryStore: Logger {
}
}
}
fileprivate func getMessageIdInt(account: BareJID, jid: BareJID, stanzaId: String?) -> Int? {
guard stanzaId != nil else {
return nil;
}
let idParams: [String: Any?] = ["account": account, "jid": jid, "stanzaId": stanzaId!];
guard let msgId = try! self.msgGetIdWithAccountPariticipantAndStanzaIdStmt.scalar(idParams) else {
return nil;
}
return msgId;
}
open func deleteMessages(for account: BareJID, with jid: BareJID) {
let params:[String:Any?] = ["account":account, "jid":jid];
_ = try! self.msgsDeleteStmt.update(params);
}
open func updateItemState(for account: BareJID, with jid: BareJID, stanzaId: String?, from oldState: MessageState, to newState: MessageState, withTimestamp timestamp: Date? = nil) {
dispatcher.async {
guard let itemId = self.getMessageIdInt(account: account, jid: jid, stanzaId: stanzaId) else {
return;
}
self.updateItemState(for: account, with: jid, itemId: itemId, from: oldState, to: newState, withTimestamp: timestamp);
}
}
open func updateItemState(for account: BareJID, with jid: BareJID, itemId: Int, from oldState: MessageState, to newState: MessageState, withTimestamp timestamp: Date? = nil) {
dispatcher.async {
let params: [String: Any?] = ["id": itemId, "oldState": oldState.rawValue, "newState": newState.rawValue, "timestamp": timestamp];
if try! self.msgUpdateStateStmt.update(params) > 0 {
self.itemUpdated(withId: itemId, for: account, with: jid);
}
}
}
open func updateItem(for account: BareJID, with jid: BareJID, id: Int, updateAppendix updateFn: @escaping (inout ChatAttachmentAppendix)->Void) {
dispatcher.async {
@ -324,7 +506,7 @@ open class DBChatHistoryStore: Logger {
}
}
}
open func updateItem(id: Int, updateAppendix updateFn: @escaping (inout ChatAttachmentAppendix)->Void) {
dispatcher.async {
var params: [String: Any?] = ["id": id];
@ -346,20 +528,21 @@ open class DBChatHistoryStore: Logger {
}
}
open func loadUnsentMessage(for account: BareJID, completionHandler: @escaping (BareJID,BareJID,String,String,MessageEncryption)->Void) {
func loadUnsentMessage(for account: BareJID, completionHandler: @escaping (BareJID,BareJID,String,String,MessageEncryption, ItemType)->Void) {
dispatcher.async {
try! self.getUnsentMessagesForAccountStmt.query(["account": account] as [String : Any?], forEach: { (cursor) in
let jid: BareJID = cursor["jid"]!;
let type = ItemType(rawValue: cursor["item_type"]!)!;
let data: String = cursor["data"]!;
let stanzaId: String = cursor["stanza_id"]!;
let encryption: MessageEncryption = MessageEncryption(rawValue: cursor["encryption"] ?? 0) ?? .none;
completionHandler(account, jid, data, stanzaId, encryption);
completionHandler(account, jid, data, stanzaId, encryption, type);
});
}
}
func itemUpdated(withId id: Int, for account: BareJID, with jid: BareJID) {
open func itemUpdated(withId id: Int, for account: BareJID, with jid: BareJID) {
dispatcher.async {
let params: [String: Any?] = ["id": id]
try! self.getChatMessageWithIdStmt.query(params, forEach: { (cursor) in
@ -370,19 +553,72 @@ open class DBChatHistoryStore: Logger {
});
}
}
fileprivate func itemRemoved(withId id: Int, for account: BareJID, with jid: BareJID) {
dispatcher.async {
NotificationCenter.default.post(name: DBChatHistoryStore.MESSAGE_REMOVED, object: DeletedMessage(id: id, account: account, jid: jid));
}
}
@objc open func accountRemoved(_ notification: NSNotification) {
if let data = notification.userInfo {
let accountStr = data["account"] as! String;
_ = try! dbConnection.prepareStatement("DELETE FROM chat_history WHERE account = ?").update(accountStr);
open func history(for account: BareJID, jid: BareJID, before: Int? = nil, limit: Int, completionHandler: @escaping (([ChatViewItemProtocol]) -> Void)) {
dispatcher.async {
if before != nil {
let params: [String: Any?] = ["account": account, "jid": jid, "msgId": before!, "showLinkPreviews": self.linkPreviews];
let offset = try! self.getChatMessagePosition.scalar(params)!;
completionHandler(self.history(for: account, jid: jid, offset: offset, limit: limit));
} else {
completionHandler(self.history(for: account, jid: jid, offset: 0, limit: limit));
}
}
}
open func history(for account: BareJID, jid: BareJID, before: Int? = nil, limit: Int) -> [ChatViewItemProtocol] {
return dispatcher.sync {
if before != nil {
let offset = try! getChatMessagePosition.scalar(["account": account, "jid": jid, "msgId": before!, "showLinkPreviews": linkPreviews])!;
return history(for: account, jid: jid, offset: offset, limit: limit);
} else {
return history(for: account, jid: jid, offset: 0, limit: limit);
}
}
}
// open func searchHistory(for account: BareJID? = nil, with jid: BareJID? = nil, search: String, completionHandler: @escaping ([ChatViewItemProtocol])->Void) {
// dispatcher.async {
// let tokens = search.unicodeScalars.split(whereSeparator: { (c) -> Bool in
// return CharacterSet.punctuationCharacters.contains(c) || CharacterSet.whitespacesAndNewlines.contains(c);
// }).map({ (s) -> String in
// return String(s) + "*";
// });
// let query = tokens.joined(separator: " + ");
// print("searching for:", tokens, "query:", query);
// let params: [String: Any?] = ["account": account, "jid": jid, "query": query];
// let items = (try? self.searchHistoryStmt.query(params, map: { (cursor) -> ChatViewItemProtocol? in
// guard let account: BareJID = cursor["account"], let jid: BareJID = cursor["jid"] else {
// return nil;
// }
// return self.itemFrom(cursor: cursor, for: account, with: jid);
// })) ?? [];
// completionHandler(items);
// }
// }
fileprivate func history(for account: BareJID, jid: BareJID, offset: Int, limit: Int) -> [ChatViewItemProtocol] {
let params: [String: Any?] = ["account": account, "jid": jid, "offset": offset, "limit": limit, "showLinkPreviews": linkPreviews];
return try! getChatMessagesStmt.query(params) { (cursor) -> ChatViewItemProtocol? in
return itemFrom(cursor: cursor, for: account, with: jid);
}
}
// public func checkItemAlreadyAdded(for account: BareJID, with jid: BareJID, authorNickname: String?, type: ItemType, timestamp: Date, direction: MessageDirection, stanzaId: String?, data: String?) -> Bool {
// let range = stanzaId == nil ? 5.0 : 60.0;
// let ts_from = timestamp.addingTimeInterval(-60 * range);
// let ts_to = timestamp.addingTimeInterval(60 * range);
//
// let params: [String: Any?] = ["account": account, "jid": jid, "ts_from": ts_from, "ts_to": ts_to, "item_type": type.rawValue, "direction": direction.rawValue, "stanza_id": stanzaId, "data": data, "author_nickname": authorNickname];
//
// return (try! checkItemAlreadyAddedStmt.scalar(params) ?? 0) > 0;
// }
public func loadAttachments(for account: BareJID, with jid: BareJID, completionHandler: @escaping ([ChatAttachment])->Void) {
let params: [String: Any?] = ["account": account, "jid": jid];
@ -401,38 +637,52 @@ open class DBChatHistoryStore: Logger {
return false;
}
}
func itemFrom(cursor: DBCursor, for account: BareJID, with jid: BareJID) -> ChatViewItemProtocol? {
let id: Int = cursor["id"]!;
let stateInt: Int = cursor["state"]!;
let timestamp: Date = cursor["timestamp"]!;
guard let entryType = ItemType(rawValue: cursor["item_type"]!) else {
return nil;
}
let authorNickname: String? = cursor["author_nickname"];
let authorJid: BareJID? = cursor["author_jid"];
let recipientNickname: String? = cursor["recipient_nickname"];
let participantId: String? = cursor["participant_id"];
let encryption: MessageEncryption = MessageEncryption(rawValue: cursor["encryption"] ?? 0) ?? .none;
let encryptionFingerprint: String? = cursor["fingerprint"];
let error: String? = cursor["error"];
//let appendix: String? = cursor["appendix"];
// maybe we should have a "supplement" object which would provide additional info? such as additional data, etc..
switch entryType {
case .message:
let message: String = cursor["data"]!;
return ChatMessage(id: id, timestamp: timestamp, account: account, jid: jid, state: MessageState(rawValue: stateInt)!, message: message, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: error);
var preview: [String: String]? = nil;
if let previewStr: String = cursor["preview"] {
preview = [:];
previewStr.split(separator: "\n").forEach { (line) in
let tmp = line.split(separator: "\t");
preview?[String(tmp[0])] = String(tmp[1]);
}
}
return ChatMessage(id: id, timestamp: timestamp, account: account, jid: jid, state: MessageState(rawValue: stateInt)!, message: message, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, participantId: participantId, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: error);
case .attachment:
let url: String = cursor["data"]!;
let appendix = parseAttachmentAppendix(string: cursor["appendix"]);
return ChatAttachment(id: id, timestamp: timestamp, account: account, jid: jid, state: MessageState(rawValue: stateInt)!, url: url, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, encryption: encryption, encryptionFingerprint: encryptionFingerprint, appendix: appendix, error: error);
return ChatAttachment(id: id, timestamp: timestamp, account: account, jid: jid, state: MessageState(rawValue: stateInt)!, url: url, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, participantId: participantId, encryption: encryption, encryptionFingerprint: encryptionFingerprint, appendix: appendix, error: error);
case .linkPreview:
let url: String = cursor["data"]!;
return ChatLinkPreview(id: id, timestamp: timestamp, account: account, jid: jid, state: MessageState(rawValue: stateInt)!, url: url, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: error)
return ChatLinkPreview(id: id, timestamp: timestamp, account: account, jid: jid, state: MessageState(rawValue: stateInt)!, url: url, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, participantId: participantId, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: error)
}
}
fileprivate func parseAttachmentAppendix(string: String?) -> ChatAttachmentAppendix {
@ -443,73 +693,14 @@ open class DBChatHistoryStore: Logger {
}
}
public enum MessageState: Int {
case incoming = 0
case outgoing = 1
case incoming_unread = 2
case outgoing_unsent = 3
case outgoing_error = 4 //7
case incoming_error = 5 //9
case incoming_error_unread = 6 //8
case outgoing_error_unread = 7 //6
case outgoing_delivered = 9 //4
case outgoing_read = 11//5
var direction: MessageDirection {
switch self {
case .incoming, .incoming_unread, .incoming_error_unread, .incoming_error:
return .incoming;
case .outgoing, .outgoing_unsent, .outgoing_delivered, .outgoing_read, .outgoing_error_unread, .outgoing_error:
return .outgoing;
}
}
var isError: Bool {
switch self {
case .incoming_error, .incoming_error_unread, .outgoing_error, .outgoing_error_unread:
return true;
default:
return false;
}
}
var isUnread: Bool {
switch self {
case .incoming_unread, .incoming_error_unread, .outgoing_error_unread:
return true;
default:
return false;
}
}
func toRead() -> MessageState {
switch self {
case .incoming_unread:
return .incoming;
case .incoming_error_unread:
return .incoming_error;
case .outgoing_error_unread:
return .outgoing_error;
default:
return self;
}
}
}
public enum MessageDirection: Int {
case incoming = 0
case outgoing = 1
}
public enum ItemType:Int {
public enum ItemType: Int {
case message = 0
case attachment = 1
@available(iOS 13, *)
// how about new type called link preview? this way we would have a far less data kept in a single item..
// we could even have them separated to the new item/entry during adding message to the store..
@available(iOS 13.0, *)
case linkPreview = 2
// with that in place we can have separate metadata kept "per" message as it is only one, so message id can be id of associated metadata..
}
class DeletedMessage: ChatViewItemProtocol {
@ -536,5 +727,5 @@ class DeletedMessage: ChatViewItemProtocol {
func copyText(withTimestamp: Bool, withSender: Bool) -> String? {
return nil;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,71 @@
//
// DBRoomStore.swift
//
// Siskin IM
// Copyright (C) 2020 "Tigase, Inc." <office@tigase.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. Look for COPYING file in the top folder.
// If not, see https://www.gnu.org/licenses/.
//
import Foundation
import TigaseSwift
open class DBRoomStore: RoomStore {
public let dispatcher: QueueDispatcher;
private let sessionObject: SessionObject;
public var rooms: [Room] {
return store.getChats(for: sessionObject.userBareJid!).filter({ $0 is Room}).map({ $0 as! Room });
}
private let store = DBChatStore.instance;
public init(sessionObject: SessionObject) {
self.sessionObject = sessionObject;
self.dispatcher = store.dispatcher;
}
deinit {
self.store.unloadChats(for: self.sessionObject.userBareJid!);
}
public func room(for jid: BareJID) -> Room? {
return store.getChat(for: sessionObject.userBareJid!, with: jid) as? Room;
}
public func createRoom(roomJid: BareJID, nickname: String, password: String?) -> Result<Room, ErrorCondition> {
switch store.createRoom(for: sessionObject.userBareJid!, context: sessionObject.context, roomJid: roomJid, nickname: nickname, password: password) {
case .success(let room):
return .success(room as Room);
case .failure(let error):
return .failure(error);
}
}
public func close(room: Room) -> Bool {
return store.close(for: sessionObject.userBareJid!, chat: room);
}
public func initialize() {
store.loadChats(for: sessionObject.userBareJid!, context: sessionObject.context);
}
public func deinitialize() {
store.unloadChats(for: sessionObject.userBareJid!);
}
}

View file

@ -1,113 +0,0 @@
//
// DBRoomsManager.swift
//
// Siskin IM
// Copyright (C) 2016 "Tigase, Inc." <office@tigase.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. Look for COPYING file in the top folder.
// If not, see https://www.gnu.org/licenses/.
//
import Foundation
import TigaseSwift
open class DBRoomsManager: DefaultRoomsManager {
fileprivate let store: DBChatStore;
public convenience init() {
self.init(store: DBChatStore.instance);
}
public init(store: DBChatStore) {
self.store = store;
super.init(dispatcher: store.dispatcher);
}
open override func createRoomInstance(roomJid: BareJID, nickname: String, password: String?) -> Room {
let room = super.createRoomInstance(roomJid: roomJid, nickname: nickname, password: password);
return store.open(for: context.sessionObject.userBareJid!, chat: room)!;
}
open override func contains(roomJid: BareJID) -> Bool {
return getRoom(for: roomJid) != nil;
}
open override func getRoom(for roomJid: BareJID) -> Room? {
return store.getChat(for: context.sessionObject.userBareJid!, with: roomJid) as? Room;
}
open override func getRoomOrCreate(for roomJid: BareJID, nickname: String, password: String?, onCreate: @escaping (Room) -> Void) -> Room {
let room = super.createRoomInstance(roomJid: roomJid, nickname: nickname, password: password);
let account: BareJID = context.sessionObject.userBareJid!;
let dbRoom: DBRoom = store.open(for: account, chat: room)!;
if dbRoom.state == .not_joined {
onCreate(dbRoom);
}
return dbRoom;
}
open override func getRooms() -> [Room] {
return store.getChats(for: context.sessionObject.userBareJid!).filter({ (item) -> Bool in
return item is Room;
}).map({ item -> Room in item as! Room });
}
open override func register(room: Room) {
// nothing to do....
}
open override func remove(room: Room) {
_ = store.close(for: context!.sessionObject.userBareJid!, chat: room);
}
open override func initialize() {
super.initialize();
store.loadChats(for: context!.sessionObject.userBareJid!, context: context);
}
public func deinitialize() {
store.unloadChats(for: context!.sessionObject.userBareJid!);
}
}
public struct RoomOptions: Codable, ChatOptionsProtocol {
public var notifications: ConversationNotification;
init() {
notifications = .mention;
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self);
if let val = try container.decodeIfPresent(String.self, forKey: .notifications) {
notifications = ConversationNotification(rawValue: val) ?? .mention;
} else {
notifications = .mention;
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self);
if notifications != .mention {
try container.encode(notifications.rawValue, forKey: .notifications);
}
}
enum CodingKeys: String, CodingKey {
case notifications = "notifications"
}
}

View file

@ -23,180 +23,225 @@ import Foundation
import Shared
import TigaseSwift
open class DBRosterStoreWrapper: RosterStore {
class DBRosterStoreWrapper: RosterStore {
fileprivate let cache: NSCache<NSString, RosterItem>?;
fileprivate var roster = [JID: DBRosterItem]();
fileprivate var queue = DispatchQueue(label: "db_roster_store_wrapper", attributes: DispatchQueue.Attributes.concurrent);
fileprivate let sessionObject: SessionObject;
fileprivate let store:DBRosterStore;
fileprivate let store = DBRosterStore.instance;
let dispatcher: QueueDispatcher;
override open var count: Int {
return store.count(for: sessionObject);
open override var count:Int {
get {
return queue.sync {
return self.roster.count;
}
}
}
init(sessionObject: SessionObject, store: DBRosterStore, useCache: Bool = true) {
public init(sessionObject: SessionObject) {
self.sessionObject = sessionObject;
self.store = store;
self.dispatcher = store.dispatcher;
self.cache = useCache ? NSCache() : nil;
self.cache?.countLimit = 100;
self.cache?.totalCostLimit = 1 * 1024 * 1024;
super.init();
}
override open func addItem(_ item:RosterItem) {
dispatcher.sync(flags: .barrier) {
if let dbItem = store.addItem(for: sessionObject, item: item) {
cache?.setObject(dbItem, forKey: createKey(jid: dbItem.jid) as NSString);
open func initialize() {
queue.sync(flags: .barrier) {
self.store.getAll(for: sessionObject.userBareJid!).forEach { item in
roster[item.jid] = item;
}
}
}
override open func get(for jid:JID) -> RosterItem? {
return dispatcher.sync {
if let item = cache?.object(forKey: createKey(jid: jid) as NSString) {
open override func addItem(_ item:RosterItem) {
queue.async(flags: .barrier, execute: {
let dbItem = self.store.set(for: self.sessionObject.userBareJid!, item: item);
self.roster[item.jid] = dbItem;
})
}
open func getJids() -> [JID] {
var result = [JID]();
queue.sync {
self.roster.keys.forEach({ (jid) in
result.append(jid);
});
}
return result;
}
open func getAll() -> [RosterItem] {
return queue.sync {
return self.roster.values.map({ (item) -> RosterItem in
return item;
}
if let item = store.get(for: sessionObject, jid: jid) {
cache?.setObject(item, forKey: createKey(jid: jid) as NSString);
return item;
}
return nil;
})
}
}
override open func removeAll() {
dispatcher.sync(flags: .barrier) {
cache?.removeAllObjects();
store.removeAll(for: sessionObject);
open override func get(for jid:JID) -> RosterItem? {
return queue.sync {
return self.roster[jid];
}
}
override open func removeItem(for jid:JID) {
dispatcher.sync(flags: .barrier) {
cache?.removeObject(forKey: createKey(jid: jid) as NSString);
store.removeItem(for: sessionObject, jid: jid);
open override func removeItem(for jid:JID) {
queue.async(flags: .barrier, execute: {
guard let item = self.roster.removeValue(forKey: jid) else {
return;
}
self.store.remove(for: self.sessionObject.userBareJid!, item: item)
})
}
open override func removeAll() {
queue.async(flags: .barrier) {
self.store.removeAll(for: self.sessionObject.userBareJid!);
self.roster.removeAll();
}
}
fileprivate func createKey(jid: JID) -> String {
guard jid.resource != nil else {
return jid.bareJid.stringValue.lowercased();
}
return "\(jid.bareJid.stringValue.lowercased())/\(jid.resource!)";
}
}
open class DBRosterStore: RosterCacheProvider {
static let ITEM_UPDATED = Notification.Name("rosterItemUpdated");
static let instance: DBRosterStore = DBRosterStore.init();
fileprivate let dbConnection: DBConnection;
fileprivate lazy var countItemsStmt: DBStatement! = try? self.dbConnection.prepareStatement("SELECT count(id) FROM roster_items WHERE account = :account");
fileprivate lazy var deleteItemStmt: DBStatement! = try? self.dbConnection.prepareStatement("DELETE FROM roster_items WHERE account = :account AND jid = :jid");
fileprivate lazy var deleteItemGroupsStmt: DBStatement! = try? self.dbConnection.prepareStatement("DELETE FROM roster_items_groups WHERE item_id IN (SELECT id FROM roster_items WHERE account = :account AND jid = :jid)");
fileprivate lazy var deleteItemsStmt: DBStatement! = try? self.dbConnection.prepareStatement("DELETE FROM roster_items WHERE account = :account");
fileprivate lazy var deleteItemsGroupsStmt: DBStatement! = try? self.dbConnection.prepareStatement("DELETE FROM roster_items_groups WHERE item_id IN (SELECT id FROM roster_items WHERE account = :account)");
public let dispatcher: QueueDispatcher;
fileprivate lazy var getGroupIdStmt: DBStatement! = try? self.dbConnection.prepareStatement("SELECT id from roster_groups WHERE name = :name");
fileprivate lazy var getItemGroupsStmt: DBStatement! = try? self.dbConnection.prepareStatement("SELECT name FROM roster_groups rg INNER JOIN roster_items_groups rig ON rig.group_id = rg.id WHERE rig.item_id = :item_id");
fileprivate lazy var getItemStmt: DBStatement! = try? self.dbConnection.prepareStatement("SELECT id, name, subscription, ask FROM roster_items WHERE account = :account AND jid = :jid");
fileprivate let insertItemStmt: DBStatement;
fileprivate let updateItemStmt: DBStatement;
fileprivate let deleteItemStmt: DBStatement;
fileprivate lazy var insertGroupStmt: DBStatement! = try? self.dbConnection.prepareStatement("INSERT INTO roster_groups (name) VALUES (:name)");
fileprivate lazy var insertItemStmt: DBStatement! = try? self.dbConnection.prepareStatement("INSERT INTO roster_items (account, jid, name, subscription, timestamp, ask) VALUES (:account, :jid, :name, :subscription, :timestamp, :ask)");
fileprivate lazy var insertItemGroupStmt: DBStatement! = try? self.dbConnection.prepareStatement("INSERT INTO roster_items_groups (item_id, group_id) VALUES (:item_id, :group_id)");
fileprivate lazy var updateItemStmt: DBStatement! = try? self.dbConnection.prepareStatement("UPDATE roster_items SET name = :name, subscription = :subscription, timestamp = :timestamp, ask = :ask WHERE account = :account AND jid = :jid");
fileprivate let getAllItemsGroupsStmt: DBStatement;
fileprivate let getAllItemsStmt: DBStatement;
fileprivate var dispatcher = QueueDispatcher(label: "db_roster_store_queue");
fileprivate let insertGroupStmt: DBStatement;
fileprivate let getGroupIdStmt: DBStatement;
fileprivate let insertItemGroupStmt: DBStatement;
fileprivate let deleteItemGroupsStmt: DBStatement;
convenience init() {
self.init(dbConnection: DBConnection.main);
}
public init(dbConnection:DBConnection) {
self.dbConnection = dbConnection;
public init() {
self.dispatcher = QueueDispatcher(label: "db_roster_store");
insertItemStmt = try! DBConnection.main.prepareStatement("INSERT INTO roster_items (account, jid, name, subscription, timestamp, ask, annotations) VALUES (:account, :jid, :name, :subscription, :timestamp, :ask, :annotations)");
updateItemStmt = try! DBConnection.main.prepareStatement("UPDATE roster_items SET name = :name, subscription = :subscription, timestamp = :timestamp, ask = :ask, annotations = :annotations WHERE id = :id");
deleteItemStmt = try! DBConnection.main.prepareStatement("DELETE FROM roster_items WHERE id = :id");
getAllItemsGroupsStmt = try! DBConnection.main.prepareStatement("SELECT rig.item_id as item_id, rg.name as name FROM roster_items ri INNER JOIN roster_items_groups rig ON ri.id = rig.item_id INNER JOIN roster_groups rg ON rig.group_id = rg.id WHERE ri.account = :account");
getAllItemsStmt = try! DBConnection.main.prepareStatement("SELECT id, jid, name, subscription, ask, annotations FROM roster_items WHERE account = :account");
getGroupIdStmt = try! DBConnection.main.prepareStatement("SELECT id from roster_groups WHERE name = :name");
insertGroupStmt = try! DBConnection.main.prepareStatement("INSERT INTO roster_groups (name) VALUES (:name)");
insertItemGroupStmt = try! DBConnection.main.prepareStatement("INSERT INTO roster_items_groups (item_id, group_id) VALUES (:item_id, :group_id)");
deleteItemGroupsStmt = try! DBConnection.main.prepareStatement("DELETE FROM roster_items_groups WHERE item_id = :item_id");
NotificationCenter.default.addObserver(self, selector: #selector(DBRosterStore.accountRemoved), name: NSNotification.Name(rawValue: "accountRemoved"), object: nil);
}
open func count(for sessionObject: SessionObject) -> Int {
func getAll(for account: BareJID) -> [DBRosterItem] {
return dispatcher.sync {
let params:[String:Any?] = ["account" : sessionObject.userBareJid!.stringValue];
return try! countItemsStmt.scalar(params) ?? 0;
}
}
open func addItem(for sessionObject: SessionObject, item:RosterItem) -> RosterItem? {
return dispatcher.sync {
let params:[String:Any?] = [ "account": sessionObject.userBareJid, "jid": item.jid, "name": item.name, "subscription": String(item.subscription.rawValue), "timestamp": NSDate(), "ask": item.ask ];
let dbItem = item as? DBRosterItem ?? DBRosterItem(rosterItem: item);
if dbItem.id == nil {
// adding roster item to DB
dbItem.id = try! insertItemStmt.insert(params);
} else {
// updating roster item in DB
_ = try! updateItemStmt.update(params);
let itemGroupsDeleteParams:[String:Any?] = ["account": sessionObject.userBareJid, "jid": dbItem.jid];
_ = try! deleteItemGroupsStmt.update(itemGroupsDeleteParams);
let params: [String: Any?] = ["account": account];
var groups: [Int: [String]] = [:];
try! self.getAllItemsGroupsStmt.query(params) { cursor in
let itemId: Int = cursor["item_id"]!;
let group: String = cursor["name"]!;
var tmp = groups[itemId] ?? [];
tmp.append(group);
groups[itemId] = tmp;
}
for group in dbItem.groups {
let gparams:[String:Any?] = ["name": group];
var groupId = try! getGroupIdStmt.scalar(gparams);
if groupId == nil {
groupId = try! insertGroupStmt.insert(gparams);
return try! self.getAllItemsStmt.query(params, map: { (cursor) -> DBRosterItem? in
let itemId: Int = cursor["id"]!;
let jid: JID = cursor["jid"]!;
let name: String? = cursor["name"];
let subscription = RosterItem.Subscription(rawValue: cursor["subscription"]!)!;
let ask: Bool = cursor["ask"]!;
var annotations: [RosterItemAnnotation] = [];
if let annotationsStr: String = cursor["annotations"], let annotationsData = annotationsStr.data(using: .utf8) {
if let val = try? JSONDecoder().decode([RosterItemAnnotation].self, from: annotationsData) {
annotations = val;
}
}
let igparams:[String:Any?] = ["item_id": dbItem.id, "group_id": groupId];
_ = try! insertItemGroupStmt.insert(igparams);
}
return dbItem;
let itemGroups = groups[itemId] ?? [];
return DBRosterItem(id: itemId, jid: jid, name: name, subscription: subscription, groups: itemGroups, ask: ask, annotations: annotations);
});
}
}
open func get(for sessionObject: SessionObject, jid:JID) -> RosterItem? {
let params:[String:Any?] = [ "account" : sessionObject.userBareJid, "jid" : jid ];
func remove(for account: BareJID, item: DBRosterItem) {
dispatcher.sync {
deleteItemGroups(item: item);
let params: [String: Any?] = ["id": item.id];
_ = try! self.deleteItemStmt.update(params);
}
}
func removeAll(for account: BareJID) {
dispatcher.sync {
getAll(for: account).forEach { item in
self.remove(for: account, item: item);
}
}
}
func set(for account: BareJID, item: RosterItem) -> DBRosterItem {
return dispatcher.sync {
if let (id, name, subscription, ask): (Int, String?, RosterItem.Subscription, Bool) = try! self.getItemStmt.findFirst(params, map: { cursor in
(cursor["id"]!, cursor["name"], RosterItem.Subscription(rawValue: cursor["subscription"]!)!, cursor["ask"]!)
}) {
let groupParams: [String: Any?] = ["item_id": id];
let groups: [String] = try! self.getItemGroupsStmt.query(groupParams) { cursor in cursor["name"] }
return DBRosterItem(jid: jid, id: id, name: name, subscription: subscription, groups: groups, ask: ask);
guard let i = item as? DBRosterItem else {
let annotations = String(data: (try? JSONEncoder().encode(item.annotations)) ?? Data(), encoding: .utf8);
let params: [String: Any?] = ["account": account, "jid": item.jid, "name": item.name, "subscription": item.subscription.rawValue, "timestamp": Date(), "ask": item.ask, "annotations": annotations];
let id = try! self.insertItemStmt.insert(params)!;
let dbItem = DBRosterItem(id: id, item: item);
self.insertItemGroups(item: dbItem);
return dbItem;
}
return nil;
}
}
open func removeAll(for sessionObject: SessionObject) {
deleteAllItems(for: sessionObject.userBareJid!);
}
open func removeItem(for sessionObject: SessionObject, jid:JID) {
let params:[String:Any?] = ["account": sessionObject.userBareJid, "jid": jid];
dispatcher.sync(flags: .barrier) {
do {
_ = try self.deleteItemGroupsStmt.update(params);
_ = try self.deleteItemStmt.update(params);
} catch _ {
let annotations = String(data: (try? JSONEncoder().encode(item.annotations)) ?? Data(), encoding: .utf8);
let params: [String: Any?] = ["id": i.id, "name": i.name, "subscription": item.subscription.rawValue, "timestamp": Date(), "ask": item.ask, "annotations": annotations];
}
_ = try! self.updateItemStmt.update(params);
deleteItemGroups(item: i);
insertItemGroups(item: i);
return i;
}
}
open func getCachedVersion(_ sessionObject: SessionObject) -> String? {
fileprivate func insertItemGroups(item: DBRosterItem) {
item.groups.forEach({ group in
let groupId = ensure(group: group);
let params: [String: Any?] = ["item_id": item.id, "group_id": groupId];
_ = try! self.insertItemGroupStmt.insert(params);
})
}
fileprivate func deleteItemGroups(item: DBRosterItem) {
let params: [String: Any?] = ["item_id": item.id];
_ = try! deleteItemGroupsStmt.update(params);
}
fileprivate func ensure(group: String) -> Int {
let params: [String: Any?] = ["name": group];
guard let groupId = try! getGroupIdStmt.scalar(params) else {
return try! insertGroupStmt.insert(params)!;
}
return groupId;
}
public func getCachedVersion(_ sessionObject: SessionObject) -> String? {
return AccountManager.getAccount(for: sessionObject.userBareJid!)?.rosterVersion;
}
open func loadCachedRoster(_ sessionObject: SessionObject) -> [RosterItem] {
public func loadCachedRoster(_ sessionObject: SessionObject) -> [RosterItem] {
return [RosterItem]();
}
open func updateReceivedVersion(_ sessionObject: SessionObject, ver: String?) {
public func updateReceivedVersion(_ sessionObject: SessionObject, ver: String?) {
if let account = AccountManager.getAccount(for: sessionObject.userBareJid!) {
account.rosterVersion = ver;
AccountManager.save(account: account);
@ -206,48 +251,29 @@ open class DBRosterStore: RosterCacheProvider {
@objc open func accountRemoved(_ notification: NSNotification) {
if let data = notification.userInfo {
let accountStr = data["account"] as! String;
deleteAllItems(for: BareJID(accountStr));
removeAll(for: BareJID(accountStr));
}
}
fileprivate func deleteAllItems(for account: BareJID) {
dispatcher.sync {
do {
let params: [String:Any?] = ["account": account];
_ = try self.deleteItemsGroupsStmt.update(params);
_ = try self.deleteItemsStmt.update(params);
} catch _ {
}
}
}
}
extension RosterItemProtocol {
var id:Int? {
switch self {
case let ri as DBRosterItem:
return ri.id;
default:
return nil;
}
class RosterItemUpdated {
}
}
class DBRosterItem: RosterItem {
var id:Int? = nil;
init(jid: JID, id: Int?, name: String?, subscription: RosterItem.Subscription, groups: [String], ask: Bool) {
let id: Int;
init(id: Int, jid: JID, name: String?, subscription: RosterItem.Subscription, groups: [String], ask: Bool, annotations: [RosterItemAnnotation]) {
self.id = id;
super.init(jid: jid, name: name, subscription: subscription, groups: groups, ask: ask);
super.init(jid: jid, name: name, subscription: subscription, groups: groups, ask: ask, annotations: annotations);
}
init(rosterItem item: RosterItem) {
super.init(jid: item.jid, name: item.name, subscription: item.subscription, groups: item.groups, ask: item.ask);
convenience init(id: Int, item: RosterItem) {
self.init(id: id, jid: item.jid, name: item.name, subscription: item.subscription, groups: item.groups, ask: item.ask, annotations: item.annotations);
}
override func update(name: String?, subscription: RosterItem.Subscription, groups: [String], ask: Bool) -> RosterItem {
return DBRosterItem(jid: self.jid, id: self.id, name: name, subscription: subscription, groups: groups, ask: ask);
override func update(name: String?, subscription: RosterItem.Subscription, groups: [String], ask: Bool, annotations: [RosterItemAnnotation]) -> RosterItem {
return DBRosterItem(id: id, jid: jid, name: name, subscription: subscription, groups: groups, ask: ask, annotations: annotations);
}
}

View file

@ -0,0 +1,75 @@
//
// MessageState.swift
//
// Siskin IM
// Copyright (C) 2020 "Tigase, Inc." <office@tigase.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. Look for COPYING file in the top folder.
// If not, see https://www.gnu.org/licenses/.
//
import Foundation
public enum MessageState: Int {
// x % 2 == 0 - incoming
// x % 2 == 1 - outgoing
case incoming = 0
case outgoing = 1
case incoming_unread = 2
case outgoing_unsent = 3
case incoming_error = 4
case outgoing_error = 5
case incoming_error_unread = 6
case outgoing_error_unread = 7
case outgoing_delivered = 9
case outgoing_read = 11
var direction: MessageDirection {
switch self {
case .incoming, .incoming_unread, .incoming_error, .incoming_error_unread:
return .incoming;
case .outgoing, .outgoing_unsent, .outgoing_delivered, .outgoing_read, .outgoing_error_unread, .outgoing_error:
return .outgoing;
}
}
var isError: Bool {
switch self {
case .incoming_error, .incoming_error_unread, .outgoing_error, .outgoing_error_unread:
return true;
default:
return false;
}
}
var isUnread: Bool {
switch self {
case .incoming_unread, .incoming_error_unread, .outgoing_error_unread:
return true;
default:
return false;
}
}
}
public enum MessageDirection: Int {
case incoming = 0
case outgoing = 1
}

View file

@ -1,38 +0,0 @@
//
// CustomChatManager.swift
//
// Siskin IM
// Copyright (C) 2017 "Tigase, Inc." <office@tigase.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. Look for COPYING file in the top folder.
// If not, see https://www.gnu.org/licenses/.
//
import Foundation
import TigaseSwift
class CustomChatManager: DefaultChatManager {
public override init(context: Context, chatStore:ChatStore) {
super.init(context: context, chatStore: chatStore)
}
override func getChat(with jid: JID, thread: String?) -> Chat? {
return chatStore.getChat(with: jid.bareJid, filter: { (c) -> Bool in
return c.jid.bareJid == jid.bareJid;
});
}
}

View file

@ -54,9 +54,7 @@ class MessageEventHandler: XmppServiceEventHandler {
encryptionErrorBody = "Message was not encrypted for this device.";
encryption = .notForThisDevice;
case .duplicateMessage:
if let from = message.from?.bareJid, DBChatHistoryStore.instance.checkItemAlreadyAdded(for: account, with: from, authorNickname: nil, type: .message, timestamp: message.delay?.stamp ?? Date(), direction: account == from ? .outgoing : .incoming, stanzaId: message.getAttribute("id"), data: nil) {
return (nil, .none, nil);
}
return (nil, .none, nil);
case .notEncrypted:
encryption = .none;
default:
@ -73,7 +71,7 @@ class MessageEventHandler: XmppServiceEventHandler {
return (body, encryption, fingerprint);
}
let events: [Event] = [MessageModule.MessageReceivedEvent.TYPE, MessageDeliveryReceiptsModule.ReceiptEvent.TYPE, MessageCarbonsModule.CarbonReceivedEvent.TYPE, DiscoveryModule.ServerFeaturesReceivedEvent.TYPE, MessageArchiveManagementModule.ArchivedMessageReceivedEvent.TYPE, SessionEstablishmentModule.SessionEstablishmentSuccessEvent.TYPE, StreamManagementModule.ResumedEvent.TYPE, OMEMOModule.AvailabilityChangedEvent.TYPE];
let events: [Event] = [MessageModule.MessageReceivedEvent.TYPE, MessageDeliveryReceiptsModule.ReceiptEvent.TYPE, MessageCarbonsModule.CarbonReceivedEvent.TYPE, DiscoveryModule.AccountFeaturesReceivedEvent.TYPE, DiscoveryModule.ServerFeaturesReceivedEvent.TYPE, MessageArchiveManagementModule.ArchivedMessageReceivedEvent.TYPE, SessionEstablishmentModule.SessionEstablishmentSuccessEvent.TYPE, StreamManagementModule.ResumedEvent.TYPE, OMEMOModule.AvailabilityChangedEvent.TYPE];
init() {
}
@ -84,50 +82,8 @@ class MessageEventHandler: XmppServiceEventHandler {
guard let from = e.message.from, let account = e.sessionObject.userBareJid else {
return;
}
let (body, encryption, fingerprint) = MessageEventHandler.prepareBody(message: e.message, forAccount: account)
guard body != nil else {
return;
}
let timestamp = e.message.delay?.stamp ?? Date();
let state: MessageState = ((e.message.type ?? .chat) == .error) ? .incoming_error_unread : .incoming_unread;
var type: ItemType = .message;
if let oob = e.message.oob {
if oob == body! {
type = .attachment;
}
}
var authorNickname: String? = nil;
var recipientNickname: String? = nil;
if let room = DBChatStore.instance.getChat(for: account, with: from.bareJid) as? DBRoom {
if state.direction == .incoming {
authorNickname = from.resource;
recipientNickname = room.nickname;
} else {
authorNickname = room.nickname;
recipientNickname = e.message.to?.resource;
}
}
DBChatHistoryStore.instance.appendItem(for: account, with: from.bareJid, state: state, authorNickname: authorNickname, recipientNickname: recipientNickname, type: type, timestamp: timestamp, stanzaId: e.message.id, data: body!, chatState: e.message.chatState, errorCondition: e.message.errorCondition, errorMessage: e.message.errorText, encryption: encryption, encryptionFingerprint: fingerprint, completionHandler: nil);
if type == .message && !state.isError, #available(iOS 13.0, *) {
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue | NSTextCheckingResult.CheckingType.address.rawValue);
let matches = detector.matches(in: body!, range: NSMakeRange(0, body!.utf16.count));
matches.forEach { match in
if let url = match.url, let scheme = url.scheme, ["https", "http"].contains(scheme) {
DBChatHistoryStore.instance.appendItem(for: account, with: from.bareJid, state: state, authorNickname: authorNickname, recipientNickname: recipientNickname, type: .linkPreview, timestamp: timestamp, stanzaId: nil, data: url.absoluteString, chatState: e.message.chatState, errorCondition: e.message.errorCondition, errorMessage: e.message.errorText, encryption: encryption, encryptionFingerprint: fingerprint, completionHandler: nil);
}
if let address = match.components {
let query = address.values.joined(separator: ",").addingPercentEncoding(withAllowedCharacters: .urlHostAllowed);
let mapUrl = URL(string: "http://maps.apple.com/?q=\(query!)")!;
DBChatHistoryStore.instance.appendItem(for: account, with: from.bareJid, state: state, authorNickname: authorNickname, recipientNickname: recipientNickname, type: .linkPreview, timestamp: timestamp, stanzaId: nil, data: mapUrl.absoluteString, chatState: e.message.chatState, errorCondition: e.message.errorCondition, errorMessage: e.message.errorText, encryption: encryption, encryptionFingerprint: fingerprint, completionHandler: nil);
}
}
}
DBChatHistoryStore.instance.append(for: account, message: e.message, source: .stream);
case let e as MessageDeliveryReceiptsModule.ReceiptEvent:
guard let from = e.message.from?.bareJid, let account = e.sessionObject.userBareJid else {
return;
@ -135,11 +91,15 @@ class MessageEventHandler: XmppServiceEventHandler {
DBChatHistoryStore.instance.updateItemState(for: account, with: from, stanzaId: e.messageId, from: .outgoing, to: .outgoing_delivered);
case let e as SessionEstablishmentModule.SessionEstablishmentSuccessEvent:
let account = e.sessionObject.userBareJid!;
MessageEventHandler.syncMessages(for: account);
MessageEventHandler.scheduleMessageSync(for: account);
sendUnsentMessages(for: account);
case let e as StreamManagementModule.ResumedEvent:
let account = e.sessionObject.userBareJid!;
sendUnsentMessages(for: account);
case let e as DiscoveryModule.AccountFeaturesReceivedEvent:
if let account = e.sessionObject.userBareJid, let mamModule: MessageArchiveManagementModule = XmppService.instance.getClient(for: account)?.modulesManager.getModule(MessageArchiveManagementModule.ID), mamModule.isAvailable {
MessageEventHandler.syncMessagesScheduled(for: account);
}
case let e as DiscoveryModule.ServerFeaturesReceivedEvent:
guard Settings.enableMessageCarbons.bool() else {
return;
@ -156,120 +116,16 @@ class MessageEventHandler: XmppServiceEventHandler {
mcModule.enable();
case let e as MessageCarbonsModule.CarbonReceivedEvent:
guard let account = e.sessionObject.userBareJid, let from = e.message.from, let to = e.message.to else {
return;
}
let (body, encryption,fingerprint) = MessageEventHandler.prepareBody(message: e.message, forAccount: account);
guard body != nil else {
// if Settings.markMessageDeliveredToOtherResourceAsRead.bool(), let delivery = e.message.messageDelivery, e.action == .sent {
// switch delivery {
// case .received(let msgId):
// DBChatHistoryStore.instance.markAsRead(for: from.bareJid, with: to.bareJid, messageId: msgId);
// break;
// default:
// break;
// }
// }
return;
}
let jid = account == from.bareJid ? to.bareJid : from.bareJid;
let timestamp = e.message.delay?.stamp ?? Date();
let state: MessageState = calculateState(direction: account == from.bareJid ? .outgoing : .incoming, error: ((e.message.type ?? .chat) == .error), unread: /*!Settings.markMessageCarbonsAsRead.bool()*/ true);
var type: ItemType = .message;
if let oob = e.message.oob {
if oob == body! {
type = .attachment;
}
}
var authorNickname: String? = nil;
var recipientNickname: String? = nil;
if let room = DBChatStore.instance.getChat(for: account, with: jid) as? DBRoom {
// carbons should not copy PM messages
return;
}
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: state, authorNickname: authorNickname, recipientNickname: recipientNickname, type: type, timestamp: timestamp, stanzaId: e.message.id, data: body!, errorCondition: e.message.errorCondition, errorMessage: e.message.errorText, encryption: encryption, encryptionFingerprint: fingerprint, completionHandler: { (msgId) in
if state.direction == .outgoing {
DBChatHistoryStore.instance.markAsRead(for: account, with: jid, before: timestamp);
}
});
if type == .message && !state.isError, #available(iOS 13.0, *) {
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue | NSTextCheckingResult.CheckingType.address.rawValue);
let matches = detector.matches(in: body!, range: NSMakeRange(0, body!.utf16.count));
matches.forEach { match in
if let url = match.url, let scheme = url.scheme, ["https", "http"].contains(scheme) {
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: state, authorNickname: authorNickname, recipientNickname: recipientNickname, type: .linkPreview, timestamp: timestamp, stanzaId: nil, data: url.absoluteString, chatState: e.message.chatState, errorCondition: e.message.errorCondition, errorMessage: e.message.errorText, encryption: encryption, encryptionFingerprint: fingerprint, completionHandler: nil);
}
if let address = match.components {
let query = address.values.joined(separator: ",").addingPercentEncoding(withAllowedCharacters: .urlHostAllowed);
let mapUrl = URL(string: "http://maps.apple.com/?q=\(query!)")!;
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: state, authorNickname: authorNickname, recipientNickname: recipientNickname, type: .linkPreview, timestamp: timestamp, stanzaId: nil, data: mapUrl.absoluteString, errorCondition: e.message.errorCondition, errorMessage: e.message.errorText, encryption: encryption, encryptionFingerprint: fingerprint, completionHandler: nil);
}
}
}
DBChatHistoryStore.instance.append(for: account, message: e.message, source: .carbons(action: e.action));
case let e as MessageArchiveManagementModule.ArchivedMessageReceivedEvent:
guard let account = e.sessionObject.userBareJid, let from = e.message.from, let to = e.message.to else {
return;
}
let (body, encryption, fingerprint) = MessageEventHandler.prepareBody(message: e.message, forAccount: account)
guard body != nil else {
return;
}
var type: ItemType = .message;
if let oob = e.message.oob {
if oob == body {
type = .attachment;
}
}
let jid = account == from.bareJid ? to.bareJid : from.bareJid;
let timestamp = e.timestamp!;
var state: MessageState = calculateState(direction: account == from.bareJid ? .outgoing : .incoming, error: ((e.message.type ?? .chat) == .error), unread: false);
var authorNickname: String? = nil;
var recipientNickname: String? = nil;
if let room = DBChatStore.instance.getChat(for: account, with: jid) as? DBRoom {
if room.nickname == from.resource {
if state.isError {
state = .incoming_error;
} else {
state = .incoming;
}
} else {
if state.isError {
state = .outgoing_error;
} else {
state = .outgoing;
}
}
if state.direction == .incoming {
authorNickname = from.resource;
recipientNickname = room.nickname;
} else {
authorNickname = room.nickname;
recipientNickname = e.message.to?.resource;
}
}
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: state, authorNickname: authorNickname, recipientNickname: recipientNickname, type: type, timestamp: timestamp, stanzaId: e.message.id, data: body!, errorCondition: e.message.errorCondition, errorMessage: e.message.errorText, encryption: encryption, encryptionFingerprint: fingerprint, completionHandler: nil);
if type == .message && !state.isError, #available(iOS 13.0, *) {
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue | NSTextCheckingResult.CheckingType.address.rawValue);
let matches = detector.matches(in: body!, range: NSMakeRange(0, body!.utf16.count));
matches.forEach { match in
if let url = match.url, let scheme = url.scheme, ["https", "http"].contains(scheme) {
DBChatHistoryStore.instance.appendItem(for: account, with: from.bareJid, state: state, authorNickname: authorNickname, recipientNickname: recipientNickname, type: .linkPreview, timestamp: timestamp, stanzaId: nil, data: url.absoluteString, errorCondition: e.message.errorCondition, errorMessage: e.message.errorText, encryption: encryption, encryptionFingerprint: fingerprint, completionHandler: nil);
}
if let address = match.components {
let query = address.values.joined(separator: ",").addingPercentEncoding(withAllowedCharacters: .urlHostAllowed);
let mapUrl = URL(string: "http://maps.apple.com/?q=\(query!)")!;
DBChatHistoryStore.instance.appendItem(for: account, with: from.bareJid, state: state, authorNickname: authorNickname, recipientNickname: recipientNickname, type: .linkPreview, timestamp: timestamp, stanzaId: nil, data: mapUrl.absoluteString, errorCondition: e.message.errorCondition, errorMessage: e.message.errorText, encryption: encryption, encryptionFingerprint: fingerprint, completionHandler: nil);
}
}
}
DBChatHistoryStore.instance.append(for: account, message: e.message, source: .archive(source: e.source, version: e.version, messageId: e.messageId, timestamp: e.timestamp));
case let e as OMEMOModule.AvailabilityChangedEvent:
NotificationCenter.default.post(name: MessageEventHandler.OMEMO_AVAILABILITY_CHANGED, object: e);
default:
@ -307,13 +163,15 @@ class MessageEventHandler: XmppServiceEventHandler {
case .omemo:
if stanzaId == nil {
let fingerprint = DBOMEMOStore.instance.identityFingerprint(forAccount: account, andAddress: SignalAddress(name: account.stringValue, deviceId: Int32(bitPattern: DBOMEMOStore.instance.localRegistrationId(forAccount: account)!)));
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: .outgoing_unsent, type: url == nil ? .message : .attachment, timestamp: Date(), stanzaId: message.id, data: msg, encryption: .decrypted, encryptionFingerprint: fingerprint, chatAttachmentAppendix: chatAttachmentAppendix, completionHandler: messageStored);
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: .outgoing_unsent, authorNickname: nil, authorJid: nil, recipientNickname: nil, participantId: nil, type: url == nil ? .message : .attachment, timestamp: Date(), stanzaId: message.id, serverMsgId: nil, remoteMsgId: nil, data: msg, encryption: .decrypted, encryptionFingerprint: fingerprint, chatAttachmentAppendix: chatAttachmentAppendix, linkPreviewAction: .none, completionHandler: messageStored);
}
XmppService.instance.tasksQueue.schedule(for: jid, task: { (completionHandler) in
sendEncryptedMessage(message, from: account, completionHandler: { result in
switch result {
case .success(_):
DBChatHistoryStore.instance.updateItemState(for: account, with: jid, stanzaId: message.id!, from: .outgoing_unsent, to: .outgoing, withTimestamp: Date());
let timestamp = Date();
DBChatHistoryStore.instance.updateItemState(for: account, with: jid, stanzaId: message.id!, from: .outgoing_unsent, to: .outgoing, withTimestamp: timestamp);
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: .outgoing, authorNickname: nil, authorJid: nil, recipientNickname: nil, participantId: nil, type: url == nil ? .message : .attachment, timestamp: timestamp, stanzaId: nil, serverMsgId: nil, remoteMsgId: nil, data: msg, encryption: .decrypted, encryptionFingerprint: nil, chatAttachmentAppendix: chatAttachmentAppendix, linkPreviewAction: .only, completionHandler: messageStored);
case .failure(let err):
let condition = (err is ErrorCondition) ? (err as? ErrorCondition) : nil;
guard condition == nil || condition! != .gone else {
@ -340,7 +198,7 @@ class MessageEventHandler: XmppServiceEventHandler {
message.oob = url;
let type: ItemType = url == nil ? .message : .attachment;
if stanzaId == nil {
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: .outgoing_unsent, type: type, timestamp: Date(), stanzaId: message.id, data: msg, encryption: .none, encryptionFingerprint: nil, chatAttachmentAppendix: chatAttachmentAppendix, completionHandler: messageStored);
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: .outgoing_unsent, authorNickname: nil, authorJid: nil, recipientNickname: nil, participantId: nil, type: type, timestamp: Date(), stanzaId: message.id, serverMsgId: nil, remoteMsgId: nil, data: msg, encryption: .none, encryptionFingerprint: nil, chatAttachmentAppendix: chatAttachmentAppendix, linkPreviewAction: .none, completionHandler: messageStored);
}
XmppService.instance.tasksQueue.schedule(for: jid, task: { (completionHandler) in
sendUnencryptedMessage(message, from: account, completionHandler: { result in
@ -348,21 +206,7 @@ class MessageEventHandler: XmppServiceEventHandler {
case .success(_):
let timestamp = Date();
DBChatHistoryStore.instance.updateItemState(for: account, with: jid, stanzaId: message.id!, from: .outgoing_unsent, to: .outgoing, withTimestamp: timestamp);
if type == .message, #available(iOS 13.0, *) {
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue | NSTextCheckingResult.CheckingType.address.rawValue);
let matches = detector.matches(in: body!, range: NSMakeRange(0, body!.utf16.count));
matches.forEach { match in
if let url = match.url, let scheme = url.scheme, ["https", "http"].contains(scheme) {
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: .outgoing, type: .linkPreview, timestamp: timestamp, stanzaId: nil, data: url.absoluteString, encryption: .none, encryptionFingerprint: nil, completionHandler: nil);
}
if let address = match.components {
let query = address.values.joined(separator: ",").addingPercentEncoding(withAllowedCharacters: .urlHostAllowed);
let mapUrl = URL(string: "http://maps.apple.com/?q=\(query!)")!;
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: .outgoing, type: .linkPreview, timestamp: timestamp, stanzaId: nil, data: mapUrl.absoluteString, encryption: .none, encryptionFingerprint: nil, completionHandler: nil);
}
}
}
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: .outgoing, authorNickname: nil, authorJid: nil, recipientNickname: nil, participantId: nil, type: type, timestamp: timestamp, stanzaId: nil, serverMsgId: nil, remoteMsgId: nil, data: msg, encryption: .none, encryptionFingerprint: nil, chatAttachmentAppendix: chatAttachmentAppendix, linkPreviewAction: .only, completionHandler: nil);
case .failure(let err):
guard let condition = err as? ErrorCondition, condition != .gone else {
completionHandler();
@ -426,21 +270,51 @@ class MessageEventHandler: XmppServiceEventHandler {
}
fileprivate func sendUnsentMessages(for account: BareJID) {
DBChatHistoryStore.instance.loadUnsentMessage(for: account, completionHandler: { (account, jid, data, stanzaId, encryption) in
DBChatHistoryStore.instance.loadUnsentMessage(for: account, completionHandler: { (account, jid, data, stanzaId, encryption, type) in
var chat = DBChatStore.instance.getChat(for: account, with: jid);
if chat == nil {
chat = DBChatStore.instance.open(for: account, chat: Chat(jid: JID(jid), thread: nil));
switch DBChatStore.instance.createChat(for: account, jid: JID(jid), thread: nil) {
case .success(let dbChat):
chat = dbChat;
case .failure(let error):
chat = nil;
}
}
if let dbChat = chat as? DBChat {
let url = data.starts(with: "http:") || data.starts(with: "https:") ? data : nil
MessageEventHandler.sendMessage(chat: dbChat, body: data, url: data, stanzaId: stanzaId);
if type == .message {
MessageEventHandler.sendMessage(chat: dbChat, body: data, url: nil, stanzaId: stanzaId);
} else if type == .attachment {
MessageEventHandler.sendMessage(chat: dbChat, body: data, url: data, stanzaId: stanzaId);
}
}
});
}
fileprivate func calculateState(direction: MessageDirection, error: Bool, unread: Bool) -> MessageState {
static func calculateDirection(direction: MessageDirection, for account: BareJID, with jid: BareJID, authorNickname: String?, authorJid: BareJID?) -> MessageDirection {
if let authorJid = authorJid {
return account == authorJid ? .outgoing : .incoming;
}
guard let senderNickname = authorNickname else {
return direction;
}
if let conversation = DBChatStore.instance.getChat(for: account, with: jid) {
switch conversation {
case let channel as DBChannel:
return channel.participantId == senderNickname ? .outgoing : .incoming;
case let room as DBRoom:
return room.nickname == senderNickname ? .outgoing : .incoming;
default:
break;
}
}
return direction;
}
static func calculateState(direction: MessageDirection, isError error: Bool, isUnread unread: Bool) -> MessageState {
if direction == .incoming {
if error {
return unread ? .incoming_error_unread : .incoming_error;
@ -454,34 +328,86 @@ class MessageEventHandler: XmppServiceEventHandler {
}
}
static func syncMessages(for account: BareJID) {
private static var syncSinceQueue = DispatchQueue(label: "syncSinceQueue");
private static var syncSince: [BareJID: Date] = [:];
static func scheduleMessageSync(for account: BareJID) {
if AccountSettings.messageSyncAuto(account).bool() {
var syncPeriod = AccountSettings.messageSyncPeriod(account).double();
if syncPeriod == 0 {
syncPeriod = 72;
}
let syncMessagesSince = max(DBChatStore.instance.lastMessageTimestamp(for: account), Date(timeIntervalSinceNow: -1 * syncPeriod * 3600));
MessageEventHandler.syncMessages(for: account, since: syncMessagesSince);
// use last "received" stable stanza id for account MAM archive in case of MAM:2?
syncSinceQueue.async {
self.syncSince[account] = syncMessagesSince;
}
} else {
syncSinceQueue.async {
syncSince.removeValue(forKey: account);
}
}
}
static func syncMessages(for account: BareJID, since: Date, rsmQuery: RSM.Query? = nil) {
guard let client = XmppService.instance.getClient(for: account), client.state == .connected, let mamModule: MessageArchiveManagementModule = client.modulesManager.getModule(MessageArchiveManagementModule.ID) else {
NotificationCenter.default.post(name: MessageEventHandler.MESSAGE_SYNCHRONIZATION_FINISHED, object: self, userInfo: ["account": account]);
return;
}
let queryId = UUID().uuidString;
mamModule.queryItems(start: since, queryId: queryId, rsm: rsmQuery ?? RSM.Query(max: 150), onSuccess: { (queryid,complete,rsmResponse) in
if rsmResponse != nil && !complete {
MessageEventHandler.syncMessages(for: account, since: since, rsmQuery: rsmResponse?.next(150));
} else {
NotificationCenter.default.post(name: MessageEventHandler.MESSAGE_SYNCHRONIZATION_FINISHED, object: self, userInfo: ["account": account]);
static func syncMessagesScheduled(for account: BareJID) {
syncSinceQueue.async {
guard AccountSettings.messageSyncAuto(account).bool(), let syncMessagesSince = syncSince[account] else {
return;
}
}) { (error, stanza) in
print("could not synchronize message archive for:", account, "got", error as Any, stanza as Any);
NotificationCenter.default.post(name: MessageEventHandler.MESSAGE_SYNCHRONIZATION_FINISHED, object: self, userInfo: ["account": account]);
syncMessages(for: account, since: syncMessagesSince);
}
}
static func syncMessages(for account: BareJID, since: Date, rsmQuery: RSM.Query? = nil) {
guard let mamModule: MessageArchiveManagementModule = XmppService.instance.getClient(for: account)?.modulesManager.getModule(MessageArchiveManagementModule.ID) else {
return;
}
let queryId = UUID().uuidString;
mamModule.queryItems(start: since, queryId: queryId, rsm: rsmQuery ?? RSM.Query(max: 200), completionHandler: { (result) in
switch result {
case .success(let queryId, let completed, let rsmResponse):
if rsmResponse != nil && rsmResponse!.index != 0 && rsmResponse?.first != nil {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) {
self.syncMessages(for: account, since: since, rsmQuery: rsmResponse?.next(200));
}
}
case .failure(let errorCondition, let response):
print("could not synchronize message archive for:", errorCondition, "got", response as Any);
}
});
}
static func extractRealAuthor(from message: Message, for account: BareJID, with jid: JID) -> (String?, BareJID?, String?, String?) {
if message.type == .groupchat {
if let mix = message.mix {
let authorNickname = mix.nickname;
let authorJid = mix.jid;
return (authorNickname, authorJid, nil, jid.resource);
} else {
// in this case it is most likely MUC groupchat message..
return (message.from?.resource, nil, nil, nil);
}
} else {
// this can be 1-1 message from MUC..
if let room = DBChatStore.instance.getChat(for: account, with: jid.bareJid) as? DBRoom {
if room.nickname == message.from?.resource {
return (message.from?.resource, nil, message.to?.resource, nil);
} else {
return (message.from?.resource, nil, message.to?.resource, nil);
}
}
}
return (nil, nil, nil, nil);
}
static func itemType(fromMessage message: Message) -> ItemType {
if let oob = message.oob {
if (message.body == nil || oob == message.body), URL(string: oob) != nil {
return .attachment;
}
}
return .message;
}
}

View file

@ -66,43 +66,15 @@ class MucEventHandler: XmppServiceEventHandler {
room.subject = e.message.subject;
NotificationCenter.default.post(name: MucEventHandler.ROOM_STATUS_CHANGED, object: room);
}
if let xUser = XMucUserElement.extract(from: e.message) {
if xUser.statuses.contains(104) {
self.updateRoomName(room: room);
XmppService.instance.refreshVCard(account: room.account, for: room.roomJid, onSuccess: nil, onError: nil);
}
}
guard let body = e.message.body else {
return;
}
let authorJid = e.nickname == nil ? nil : room.presences[e.nickname!]?.jid?.bareJid;
var type: ItemType = .message;
if let oob = e.message.oob {
if oob == body {
type = .attachment;
}
}
DBChatHistoryStore.instance.appendItem(for: room.account, with: room.roomJid, state: ((e.nickname == nil) || (room.nickname != e.nickname!)) ? .incoming_unread : .outgoing, authorNickname: e.nickname, authorJid: authorJid, type: type, timestamp: e.timestamp, stanzaId: e.message.id, data: body, encryption: MessageEncryption.none, encryptionFingerprint: nil, completionHandler: nil);
if type == .message && e.message.type != StanzaType.error, #available(iOS 13.0, *) {
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue | NSTextCheckingResult.CheckingType.address.rawValue);
let matches = detector.matches(in: body, range: NSMakeRange(0, body.utf16.count));
matches.forEach { match in
if let url = match.url, let scheme = url.scheme, ["https", "http"].contains(scheme) {
DBChatHistoryStore.instance.appendItem(for: room.account, with: room.roomJid, state: ((e.nickname == nil) || (room.nickname != e.nickname!)) ? .incoming_unread : .outgoing, authorNickname: e.nickname, authorJid: authorJid, type: .linkPreview, timestamp: e.timestamp, stanzaId: nil, data: url.absoluteString, chatState: e.message.chatState, errorCondition: e.message.errorCondition, errorMessage: e.message.errorText, encryption: .none, encryptionFingerprint: nil, completionHandler: nil);
}
if let address = match.components {
let query = address.values.joined(separator: ",").addingPercentEncoding(withAllowedCharacters: .urlHostAllowed);
let mapUrl = URL(string: "http://maps.apple.com/?q=\(query!)")!;
DBChatHistoryStore.instance.appendItem(for: room.account, with: room.roomJid, state: ((e.nickname == nil) || (room.nickname != e.nickname!)) ? .incoming_unread : .outgoing, authorNickname: e.nickname, authorJid: authorJid, type: .linkPreview, timestamp: e.timestamp, stanzaId: nil, data: mapUrl.absoluteString, chatState: e.message.chatState, errorCondition: e.message.errorCondition, errorMessage: e.message.errorText, encryption: .none, encryptionFingerprint: nil, completionHandler: nil);
}
}
}
DBChatHistoryStore.instance.append(for: room.account, message: e.message, source: .stream);
case let e as MucModule.AbstractOccupantEvent:
NotificationCenter.default.post(name: MucEventHandler.ROOM_OCCUPANTS_CHANGED, object: e);
case let e as MucModule.PresenceErrorEvent:
@ -129,78 +101,7 @@ class MucEventHandler: XmppServiceEventHandler {
mucModule.leave(room: e.room);
case let e as MucModule.InvitationReceivedEvent:
NotificationCenter.default.post(name: XmppService.MUC_ROOM_INVITATION, object: e);
// guard let mucModule: MucModule = XmppService.instance.getClient(for: e.sessionObject.userBareJid!)?.modulesManager.getModule(MucModule.ID), let roomName = e.invitation.roomJid.localPart else {
// return;
// }
//
// guard !mucModule.roomsManager.contains(roomJid: e.invitation.roomJid) else {
// mucModule.decline(invitation: e.invitation, reason: nil);
// return;
// }
//
// let alert = Alert();
// alert.messageText = "Invitation to groupchat";
// if let inviter = e.invitation.inviter {
// let name = XmppService.instance.clients.values.flatMap({ (client) -> [String] in
// guard let n = client.rosterStore?.get(for: inviter)?.name else {
// return [];
// }
// return ["\(n) (\(inviter))"];
// }).first ?? inviter.stringValue;
// alert.informativeText = "User \(name) invited you (\(e.sessionObject.userBareJid!)) to the groupchat \(e.invitation.roomJid)";
// } else {
// alert.informativeText = "You (\(e.sessionObject.userBareJid!)) were invited to the groupchat \(e.invitation.roomJid)";
// }
// alert.addButton(withTitle: "Accept");
// alert.addButton(withTitle: "Decline");
//
// DispatchQueue.main.async {
// alert.run { (response) in
// if response == NSApplication.ModalResponse.alertFirstButtonReturn {
// let nickname = AccountManager.getAccount(for: e.sessionObject.userBareJid!)?.nickname ?? e.sessionObject.userBareJid!.localPart!;
// _ = mucModule.join(roomName: roomName, mucServer: e.invitation.roomJid.domain, nickname: nickname, password: e.invitation.password);
//
// PEPBookmarksModule.updateOrAdd(for: e.sessionObject.userBareJid!, bookmark: Bookmarks.Conference(name: roomName, jid: JID(BareJID(localPart: roomName, domain: e.invitation.roomJid.domain)), autojoin: true, nick: nickname, password: e.invitation.password));
// } else {
// mucModule.decline(invitation: e.invitation, reason: nil);
// }
// }
// }
break;
// case let e as MucModule.InvitationDeclinedEvent:
// if #available(OSX 10.14, *) {
// let content = UNMutableNotificationContent();
// content.title = "Invitation rejected";
// let name = XmppService.instance.clients.values.flatMap({ (client) -> [String] in
// guard let n = e.invitee != nil ? client.rosterStore?.get(for: e.invitee!)?.name : nil else {
// return [];
// }
// return [n];
// }).first ?? e.invitee?.stringValue ?? "";
//
// content.body = "User \(name) rejected invitation to room \(e.room.roomJid)";
// content.sound = UNNotificationSound.default;
// let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil);
// UNUserNotificationCenter.current().add(request) { (error) in
// print("could not show notification:", error as Any);
// }
// } else {
// let notification = NSUserNotification();
// notification.identifier = UUID().uuidString;
// notification.title = "Invitation rejected";
// let name = XmppService.instance.clients.values.flatMap({ (client) -> [String] in
// guard let n = e.invitee != nil ? client.rosterStore?.get(for: e.invitee!)?.name : nil else {
// return [];
// }
// return [n];
// }).first ?? e.invitee?.stringValue ?? "";
//
// notification.informativeText = "User \(name) rejected invitation to room \(e.room.roomJid)";
// notification.soundName = NSUserNotificationDefaultSoundName;
// notification.contentImage = NSImage(named: NSImage.userGroupName);
// NSUserNotificationCenter.default.deliver(notification);
// }
case let e as PEPBookmarksModule.BookmarksChangedEvent:
guard let client = XmppService.instance.getClient(for: e.sessionObject.userBareJid!), let mucModule: MucModule = client.modulesManager.getModule(MucModule.ID), Settings.enableBookmarksSync.bool() else {
return;
@ -221,7 +122,7 @@ class MucEventHandler: XmppServiceEventHandler {
open func sendPrivateMessage(room: DBRoom, recipientNickname: String, body: String) {
let message = room.createPrivateMessage(body, recipientNickname: recipientNickname);
DBChatHistoryStore.instance.appendItem(for: room.account, with: room.roomJid, state: .outgoing, authorNickname: room.nickname, recipientNickname: recipientNickname, type: .message, timestamp: Date(), stanzaId: message.id, data: body, encryption: .none, encryptionFingerprint: nil, chatAttachmentAppendix: nil, completionHandler: nil);
DBChatHistoryStore.instance.appendItem(for: room.account, with: room.roomJid, state: .outgoing, authorNickname: room.nickname, authorJid: nil, recipientNickname: recipientNickname, participantId: nil, type: .message, timestamp: Date(), stanzaId: message.id, serverMsgId: nil, remoteMsgId: nil, data: body, encryption: .none, encryptionFingerprint: nil, chatAttachmentAppendix: nil, linkPreviewAction: .auto, completionHandler: nil);
room.context.writer?.write(message);
}

View file

@ -110,53 +110,58 @@ class NewFeaturesDetector: XmppServiceEventHandler {
return;
}
mamModule.retrieveSettings(onSuccess: { (defValue, always, never) in
if defValue == .never {
DispatchQueue.main.async {
let controller = UIStoryboard(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "NewFeatureSuggestionView") as! NewFeatureSuggestionView;
_ = controller.view;
controller.titleField.text = "Message Archiving";
controller.iconField.image = UIImage(named: "messageArchiving")
controller.descriptionField.text = """
Your server for account \(account) supports message archiving.
When it is enabled your XMPP server will archive all messages which you exchange. This will allow any XMPP client which you use and which supports message archiving to query this archive and show you message history even if messages were sent using different XMPP client.
""";
controller.onSkip = {
controller.dismiss(animated: true, completion: onNext);
}
controller.onEnable = { (handler) in
mamModule.updateSettings(defaultValue: .always, always: always, never: never, onSuccess: { (defValue, always, never) in
DispatchQueue.main.async {
handler();
self.askToEnableMessageSync(xmppService: xmppService, account: account, onNext: onNext, completionHandler: { subcontrollers in
guard let toShow = subcontrollers.first else {
controller.dismiss(animated: true, completion: onNext);
return;
mamModule.retrieveSettings(completionHandler: { result in
switch result {
case .success(let defValue, let always, let never):
if defValue == .never {
DispatchQueue.main.async {
let controller = UIStoryboard(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "NewFeatureSuggestionView") as! NewFeatureSuggestionView;
_ = controller.view;
controller.titleField.text = "Message Archiving";
controller.iconField.image = UIImage(named: "messageArchiving")
controller.descriptionField.text = """
Your server for account \(account) supports message archiving.
When it is enabled your XMPP server will archive all messages which you exchange. This will allow any XMPP client which you use and which supports message archiving to query this archive and show you message history even if messages were sent using different XMPP client.
""";
controller.onSkip = {
controller.dismiss(animated: true, completion: onNext);
}
controller.onEnable = { (handler) in
mamModule.updateSettings(defaultValue: .always, always: always, never: never, completionHandler: { result in
switch result {
case .success(let defValue, let always, let never):
DispatchQueue.main.async {
handler();
self.askToEnableMessageSync(xmppService: xmppService, account: account, onNext: onNext, completionHandler: { subcontrollers in
guard let toShow = subcontrollers.first else {
controller.dismiss(animated: true, completion: onNext);
return;
}
controller.dismiss(animated: true, completion: {
UIApplication.shared.keyWindow?.rootViewController?.present(toShow, animated: true, completion: nil);
})
});
}
controller.dismiss(animated: true, completion: {
UIApplication.shared.keyWindow?.rootViewController?.present(toShow, animated: true, completion: nil);
})
});
}
}, onError: { (error, stanza) in
DispatchQueue.main.async {
handler();
self.showError(title: "Message Archiving Error", message: "Server \(account.domain) returned an error on the request to enable archiving. You can try to enable this feature later on from the account settings.");
}
});
};
completionHandler([controller]);
case .failure(let errorCondition, let response):
DispatchQueue.main.async {
handler();
self.showError(title: "Message Archiving Error", message: "Server \(account.domain) returned an error on the request to enable archiving. You can try to enable this feature later on from the account settings.");
}
}
});
};
completionHandler([controller]);
}
} else {
self.askToEnableMessageSync(xmppService: xmppService, account: account, onNext: onNext, completionHandler: completionHandler);
}
} else {
self.askToEnableMessageSync(xmppService: xmppService, account: account, onNext: onNext, completionHandler: completionHandler);
case .failure(let errorCondition, let response):
completionHandler([]);
}
}, onError: { (error, stanza) in
print("received an error:", error as Any, "- ignoring");
completionHandler([])
});
}
@ -186,7 +191,7 @@ Have it enabled will keep synchronized copy of your messages exchanged using \(a
AccountSettings.messageSyncPeriod(account).set(double: 24 * 7);
AccountSettings.messageSyncAuto(account).set(bool: true);
MessageEventHandler.syncMessages(for: account);
MessageEventHandler.syncMessages(for: account, since: Date().addingTimeInterval(-1 * 24 * 7 * 60 * 60));
controller.dismiss(animated: true, completion: onNext);
}

View file

@ -31,7 +31,7 @@ open class PushEventHandler: XmppServiceEventHandler {
let events: [Event] = [DiscoveryModule.AccountFeaturesReceivedEvent.TYPE];
init() {
NotificationCenter.default.addObserver(self, selector: #selector(chatDestroyed(_:)), name: DBChatStore.CHAT_DESTROYED, object: nil);
NotificationCenter.default.addObserver(self, selector: #selector(chatDestroyed(_:)), name: DBChatStore.CHAT_CLOSED, object: nil);
}
public func handle(event: Event) {

View file

@ -56,7 +56,6 @@ open class XmppService: Logger, EventHandler {
public let dbCapsCache: DBCapabilitiesCache;
public let dbChatStore: DBChatStore;
public let dbChatHistoryStore: DBChatHistoryStore;
fileprivate let dbRosterStore: DBRosterStore;
public let dbVCardsCache: DBVCardsCache;
fileprivate let avatarStore: AvatarStore;
open var applicationState: ApplicationState = .inactive {
@ -104,8 +103,7 @@ open class XmppService: Logger, EventHandler {
self.streamFeaturesCache = StreamFeaturesCache();
self.dbCapsCache = DBCapabilitiesCache(dbConnection: dbConnection);
self.dbChatStore = DBChatStore.instance;
self.dbChatHistoryStore = DBChatHistoryStore(dbConnection: dbConnection);
self.dbRosterStore = DBRosterStore(dbConnection: dbConnection);
self.dbChatHistoryStore = DBChatHistoryStore.instance;
self.dbVCardsCache = DBVCardsCache(dbConnection: dbConnection);
self.avatarStore = AvatarStore(dbConnection: dbConnection);
self.reachability = Reachability();
@ -662,17 +660,20 @@ open class XmppService: Logger, EventHandler {
_ = client.modulesManager.register(PEPUserAvatarModule());
_ = client.modulesManager.register(PEPBookmarksModule());
let rosterModule = client.modulesManager.register(RosterModule());
rosterModule.rosterStore = DBRosterStoreWrapper(sessionObject: client.sessionObject, store: dbRosterStore);
rosterModule.versionProvider = dbRosterStore;
let rosterStore = DBRosterStoreWrapper(sessionObject: client.sessionObject);
rosterStore.initialize();
rosterModule.rosterStore = rosterStore;
rosterModule.versionProvider = DBRosterStore.instance;
_ = client.modulesManager.register(PresenceModule());
let messageModule = client.modulesManager.register(MessageModule());
let chatManager = CustomChatManager(context: client.context, chatStore: DBChatStoreWrapper(sessionObject: client.sessionObject));
messageModule.chatManager = chatManager;
let chatStoreWrapper = DBChatStoreWrapper(sessionObject: client.context.sessionObject);
chatStoreWrapper.initialize();
messageModule.chatManager = DefaultChatManager(context: client.context, chatStore: chatStoreWrapper);
_ = client.modulesManager.register(MessageCarbonsModule());
_ = client.modulesManager.register(MessageArchiveManagementModule());
let mucModule = MucModule();
mucModule.roomsManager = DBRoomsManager(store: dbChatStore);
_ = client.modulesManager.register(mucModule);
let roomStore = DBRoomStore(sessionObject: client.context.sessionObject);
client.modulesManager.register(MucModule(roomsManager: DefaultRoomsManager(store: roomStore)));
roomStore.initialize();
_ = client.modulesManager.register(AdHocCommandsModule());
_ = client.modulesManager.register(SiskinPushNotificationsModule(defaultPushServiceJid: XmppService.pushServiceJid, provider: SiskinPushNotificationsModuleProvider()));
_ = client.modulesManager.register(HttpFileUploadModule());

View file

@ -151,18 +151,20 @@ class AccountSettingsViewController: CustomTableViewController {
archivingEnabledSwitch.isEnabled = false;
if (client?.state ?? SocketConnector.State.disconnected == SocketConnector.State.connected), let mamModule: MessageArchiveManagementModule = client?.modulesManager.getModule(MessageArchiveManagementModule.ID) {
mamModule.retrieveSettings(onSuccess: { (
defValue, always, never) in
DispatchQueue.main.async {
self.archivingEnabledSwitch.isEnabled = true;
self.archivingEnabledSwitch.isOn = defValue == MessageArchiveManagementModule.DefaultValue.always;
self.messageSyncAutomaticSwitch.isEnabled = self.archivingEnabledSwitch.isOn;
}
}, onError: { (error, stanza) in
DispatchQueue.main.async {
self.archivingEnabledSwitch.isOn = false;
self.archivingEnabledSwitch.isEnabled = false;
self.messageSyncAutomaticSwitch.isEnabled = self.archivingEnabledSwitch.isOn;
mamModule.retrieveSettings(completionHandler: { result in
switch result {
case .success(let defValue, let always, let never):
DispatchQueue.main.async {
self.archivingEnabledSwitch.isEnabled = true;
self.archivingEnabledSwitch.isOn = defValue == MessageArchiveManagementModule.DefaultValue.always;
self.messageSyncAutomaticSwitch.isEnabled = self.archivingEnabledSwitch.isOn;
}
case .failure(let errorCondition, let response):
DispatchQueue.main.async {
self.archivingEnabledSwitch.isOn = false;
self.archivingEnabledSwitch.isEnabled = false;
self.messageSyncAutomaticSwitch.isEnabled = self.archivingEnabledSwitch.isOn;
}
}
})
}
@ -318,22 +320,28 @@ class AccountSettingsViewController: CustomTableViewController {
let client = XmppService.instance.getClient(forJid: account);
if let mamModule: MessageArchiveManagementModule = client?.modulesManager.getModule(MessageArchiveManagementModule.ID) {
let defValue = archivingEnabledSwitch.isOn ? MessageArchiveManagementModule.DefaultValue.always : MessageArchiveManagementModule.DefaultValue.never;
mamModule.retrieveSettings(onSuccess: { (oldDefValue, always, never) in
mamModule.updateSettings(defaultValue: defValue, always: always, never: never, onSuccess: { (newDefValue, always1, never1)->Void in
mamModule.retrieveSettings(completionHandler: { result in
switch result {
case .success(let oldDefValue, let always, let never):
mamModule.updateSettings(defaultValue: defValue, always: always, never: never, completionHandler: { result in
switch result {
case .success(let newDefValue, let always, let never):
DispatchQueue.main.async {
self.archivingEnabledSwitch.isOn = newDefValue == MessageArchiveManagementModule.DefaultValue.always;
self.messageSyncAutomaticSwitch.isEnabled = self.archivingEnabledSwitch.isOn;
}
case .failure(let errorCondition, let response):
DispatchQueue.main.async {
self.archivingEnabledSwitch.isOn = oldDefValue == MessageArchiveManagementModule.DefaultValue.always;
self.messageSyncAutomaticSwitch.isEnabled = self.archivingEnabledSwitch.isOn;
}
}
});
case .failure(let errorCondition, let response):
DispatchQueue.main.async {
self.archivingEnabledSwitch.isOn = newDefValue == MessageArchiveManagementModule.DefaultValue.always;
self.archivingEnabledSwitch.isOn = !self.archivingEnabledSwitch.isOn;
self.messageSyncAutomaticSwitch.isEnabled = self.archivingEnabledSwitch.isOn;
}
}, onError: {(error,stanza)->Void in
DispatchQueue.main.async {
self.archivingEnabledSwitch.isOn = oldDefValue == MessageArchiveManagementModule.DefaultValue.always;
self.messageSyncAutomaticSwitch.isEnabled = self.archivingEnabledSwitch.isOn;
}
});
}, onError: {(error, stanza)->Void in
DispatchQueue.main.async {
self.archivingEnabledSwitch.isOn = !self.archivingEnabledSwitch.isOn;
self.messageSyncAutomaticSwitch.isEnabled = self.archivingEnabledSwitch.isOn;
}
});
}

View file

@ -74,7 +74,7 @@ class ImageCache {
let isAttachmentOnly = URL(string: item.message) != nil;
DBChatHistoryStore.instance.appendItem(for: item.account, with: item.jid, state: item.state, authorNickname: item.authorNickname, authorJid: item.authorJid, type: .attachment, timestamp: item.timestamp, stanzaId: stanzaId, data: item.message, encryption: item.encryption, encryptionFingerprint: item.encryptionFingerprint, chatAttachmentAppendix: appendix, skipItemAlreadyExists: true, completionHandler: { newId in
DBChatHistoryStore.instance.appendItem(for: item.account, with: item.jid, state: item.state, authorNickname: item.authorNickname, authorJid: item.authorJid, recipientNickname: item.recipientNickname, participantId: nil, type: .attachment, timestamp: item.timestamp, stanzaId: stanzaId, serverMsgId: nil, remoteMsgId: nil, data: item.message, encryption: item.encryption, encryptionFingerprint: item.encryptionFingerprint, chatAttachmentAppendix: appendix, linkPreviewAction: .none, completionHandler: { newId in
DownloadStore.instance.store(url, filename: filename, with: "\(newId)");
if isAttachmentOnly {
DBChatHistoryStore.instance.remove(item: item);