Added support for XEP-0380: Last Message Correction and fixed issue with context menu preview #siskinim-226

This commit is contained in:
Andrzej Wójcik 2020-07-09 14:54:22 +02:00
parent 6b919990c2
commit ee356f4b1f
No known key found for this signature in database
GPG key ID: 2BE28BB9C1B5FF02
13 changed files with 309 additions and 174 deletions

View file

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

9
Shared/db-schema-12.sql Normal file
View file

@ -0,0 +1,9 @@
BEGIN;
ALTER TABLE chat_history ADD COLUMN master_id INT;
ALTER TABLE chat_history ADD COLUMN correction_stanza_id TEXT;
ALTER TABLE chat_history ADD COLUMN correction_timestamp INTEGER;
COMMIT;
PRAGMA user_version = 12;

View file

@ -35,6 +35,7 @@
FE31DDE4201261A200C2AB1D /* DNSSrvDiskCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE31DDE3201261A200C2AB1D /* DNSSrvDiskCache.swift */; };
FE36B3C821FA52E000D1F037 /* EmptyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE36B3C721FA52E000D1F037 /* EmptyViewController.swift */; };
FE3A45CF1CE49D3300C36264 /* RosterItemEditViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3A45CE1CE49D3300C36264 /* RosterItemEditViewController.swift */; };
FE3BA0C024B61583000C80D4 /* db-schema-12.sql in Resources */ = {isa = PBXBuildFile; fileRef = FE3BA0BF24B61583000C80D4 /* db-schema-12.sql */; };
FE3DCCEE1FE18334008B6C8B /* CertificateErrorAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3DCCED1FE18334008B6C8B /* CertificateErrorAlert.swift */; };
FE3E387A242765E800D3A8E8 /* MixEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3E3879242765E700D3A8E8 /* MixEventHandler.swift */; };
FE3E387C242766A900D3A8E8 /* DBChannelStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3E387B242766A900D3A8E8 /* DBChannelStore.swift */; };
@ -304,6 +305,7 @@
FE31DDE3201261A200C2AB1D /* DNSSrvDiskCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSSrvDiskCache.swift; sourceTree = "<group>"; };
FE36B3C721FA52E000D1F037 /* EmptyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyViewController.swift; sourceTree = "<group>"; };
FE3A45CE1CE49D3300C36264 /* RosterItemEditViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RosterItemEditViewController.swift; sourceTree = "<group>"; };
FE3BA0BF24B61583000C80D4 /* db-schema-12.sql */ = {isa = PBXFileReference; lastKnownFileType = text; path = "db-schema-12.sql"; sourceTree = "<group>"; };
FE3DCCED1FE18334008B6C8B /* CertificateErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateErrorAlert.swift; sourceTree = "<group>"; };
FE3E3879242765E700D3A8E8 /* MixEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MixEventHandler.swift; sourceTree = "<group>"; };
FE3E387B242766A900D3A8E8 /* DBChannelStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBChannelStore.swift; sourceTree = "<group>"; };
@ -730,6 +732,7 @@
FECEF29723B7B838007EC323 /* db-schema-9.sql */,
FE10BCF223FD4EF000E214F3 /* db-schema-10.sql */,
FEC79198241BE89E007BE572 /* db-schema-11.sql */,
FE3BA0BF24B61583000C80D4 /* db-schema-12.sql */,
);
path = Shared;
sourceTree = "<group>";
@ -1116,6 +1119,7 @@
FEC79199241BE89E007BE572 /* db-schema-11.sql in Resources */,
FE759FED2371F213001E78D9 /* db-schema-2.sql in Resources */,
FECEF29823B7B838007EC323 /* db-schema-9.sql in Resources */,
FE3BA0C024B61583000C80D4 /* db-schema-12.sql in Resources */,
FE759FF02371F21C001E78D9 /* db-schema-4.sql in Resources */,
FE759FF32371F21C001E78D9 /* db-schema-7.sql in Resources */,
FE759FF12371F21C001E78D9 /* db-schema-5.sql in Resources */,

View file

@ -83,7 +83,7 @@ class ChannelViewController: BaseChatViewControllerWithDataSourceAndContextMenuA
let id = continuation ? "ChatTableViewMessageContinuationCell" : "ChatTableViewMessageCell";
let cell = tableView.dequeueReusableCell(withIdentifier: id, for: indexPath) as! ChatTableViewCell;
cell.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.contentView.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
// cell.nicknameLabel?.text = item.nickname;
if cell.avatarView != nil {
if let senderJid = item.state.direction == .incoming ? item.authorJid : item.account {
@ -101,7 +101,7 @@ class ChannelViewController: BaseChatViewControllerWithDataSourceAndContextMenuA
case let item as ChatAttachment:
let id = continuation ? "ChatTableViewAttachmentContinuationCell" : "ChatTableViewAttachmentCell";
let cell: AttachmentChatTableViewCell = tableView.dequeueReusableCell(withIdentifier: id, for: indexPath) as! AttachmentChatTableViewCell;
cell.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.contentView.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
if cell.avatarView != nil {
if let senderJid = item.state.direction == .incoming ? item.authorJid : item.account {
cell.avatarView?.set(name: item.authorNickname, avatar: AvatarManager.instance.avatar(for: senderJid, on: item.account), orDefault: AvatarManager.instance.defaultAvatar);
@ -121,18 +121,18 @@ class ChannelViewController: BaseChatViewControllerWithDataSourceAndContextMenuA
case let item as ChatLinkPreview:
let id = "ChatTableViewLinkPreviewCell";
let cell: LinkPreviewChatTableViewCell = tableView.dequeueReusableCell(withIdentifier: id, for: indexPath) as! LinkPreviewChatTableViewCell;
cell.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.contentView.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.set(linkPreview: item);
return cell;
case let item as SystemMessage:
let cell: ChatTableViewSystemCell = tableView.dequeueReusableCell(withIdentifier: "ChatTableViewSystemCell", for: indexPath) as! ChatTableViewSystemCell;
cell.set(item: item);
cell.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.contentView.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
return cell;
case let item as ChatInvitation:
let id = "ChatTableViewInvitationCell";
let cell: InvitationChatTableViewCell = tableView.dequeueReusableCell(withIdentifier: id, for: indexPath) as! InvitationChatTableViewCell;
cell.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.contentView.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
if cell.avatarView != nil {
if let senderJid = item.state.direction == .incoming ? item.authorJid : item.account {
cell.avatarView?.set(name: item.authorNickname, avatar: AvatarManager.instance.avatar(for: senderJid, on: item.account), orDefault: AvatarManager.instance.defaultAvatar);
@ -208,6 +208,7 @@ class ChannelViewController: BaseChatViewControllerWithDataSourceAndContextMenuA
}
let msg = self.channel!.createMessage(text);
msg.lastMessageCorrectionId = self.correctedMessageOriginId;
XmppService.instance.getClient(for: account)?.context.writer?.write(msg);
DispatchQueue.main.async {
self.messageText = nil;

View file

@ -46,12 +46,17 @@ class BaseChatViewController: UIViewController, UITextViewDelegate, ChatViewInpu
var account:BareJID!;
var jid:BareJID!;
private(set) var correctedMessageOriginId: String?;
var messageText: String? {
get {
return chatViewInputBar.text;
}
set {
chatViewInputBar.text = newValue;
if newValue == nil {
self.correctedMessageOriginId = nil;
}
}
}
@ -243,10 +248,18 @@ class BaseChatViewController: UIViewController, UITextViewDelegate, ChatViewInpu
_ = self.chatViewInputBar.resignFirstResponder();
}
func startMessageCorrection(message: String, originId: String) {
self.messageText = message;
self.correctedMessageOriginId = originId;
}
func sendMessage() {
}
func messageTextCleared() {
self.correctedMessageOriginId = nil;
}
@objc func sendMessageClicked(_ sender: Any) {
self.sendMessage();
}
@ -402,6 +415,9 @@ class ChatViewInputBar: UIView, UITextViewDelegate {
return false;
}
}
if text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
delegate?.messageTextCleared();
}
return true;
}
@ -418,4 +434,6 @@ protocol ChatViewInputBarDelegate: class {
func sendMessage();
func messageTextCleared();
}

View file

@ -76,58 +76,54 @@ class BaseChatViewControllerWithDataSourceAndContextMenuAndToolbar: BaseChatView
}
conversationLogController?.hideEditToolbar();
}
@available(iOS 13.0, *)
func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
guard let indexPath = configuration.identifier as? IndexPath else {
return nil;
}
let cell = self.tableView(tableView, cellForRowAt: indexPath);
let parameters = UIPreviewParameters();
let rect = self.conversationLogController!.tableView.rectForRow(at: indexPath);
let center = CGPoint(x: rect.midX, y: rect.midY);
let target = UIPreviewTarget(container: self.conversationLogController!.tableView, center: center, transform: CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0));
return UITargetedPreview(view: cell, parameters: parameters, target: target);
}
@available(iOS 13.0, *)
func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
guard let indexPath = configuration.identifier as? IndexPath else {
return nil;
}
let cell = self.tableView(tableView, cellForRowAt: indexPath);
let parameters = UIPreviewParameters();
let rect = self.conversationLogController!.tableView.rectForRow(at: indexPath);
let center = CGPoint(x: rect.midX, y: rect.midY);
let target = UIPreviewTarget(container: self.conversationLogController!.tableView, center: center, transform: .identity);
return UITargetedPreview(view: cell, parameters: parameters, target: target);
}
@available(iOS 13.0, *)
func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: nil) { suggestedActions -> UIMenu? in
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: {
let cell = self.tableView(tableView, cellForRowAt: indexPath);
cell.contentView.transform = .identity;
let view = UIViewController();
let size = self.conversationLogController!.tableView.rectForRow(at: indexPath).size;
print("cell:", (cell as? ChatTableViewCell)?.messageTextView.text);
view.view = cell.contentView;
view.preferredContentSize = size;
print("view size:", view.preferredContentSize)
return view;
}) { suggestedActions -> UIMenu? in
return self.prepareContextMenu(for: indexPath);
};
}
@available(iOS 13.0, *)
func prepareContextMenu(for indexPath: IndexPath) -> UIMenu? {
let items = [
var items = [
UIAction(title: "Copy", image: UIImage(systemName: "doc.on.doc"), handler: { action in
self.conversationLogController?.copyMessageInt(paths: [indexPath]);
}),
UIAction(title: "Share..", image: UIImage(systemName: "square.and.arrow.up"), handler: { action in
self.conversationLogController?.shareMessageInt(paths: [indexPath]);
}),
UIAction(title: "More..", image: UIImage(systemName: "ellipsis"), handler: { action in
guard let cell = self.conversationLogController?.tableView.cellForRow(at: indexPath) else {
return;
}
NotificationCenter.default.post(name: Notification.Name("tableViewCellShowEditToolbar"), object: cell);
})
];
if let dataSource = self.conversationLogController?.dataSource, let item = dataSource.getItem(at: indexPath.row), item.state.direction == .outgoing {
let row = indexPath.row;
if let messageItem = item as? ChatMessage, !dataSource.isAnyMatching({ $0.state.direction == .outgoing && $0 is ChatMessage }, in: 0..<row) {
items.append(UIAction(title: "Correct..", image: UIImage(systemName: "pencil.and.ellipsis.rectangle"), handler: { action in
DBChatHistoryStore.instance.originId(for: item.account, with: item.jid, id: item.id, completionHandler: { [weak self] originId in
DispatchQueue.main.async {
self?.startMessageCorrection(message: messageItem.message, originId: originId)
}
});
}));
}
}
items.append(contentsOf: [
UIAction(title: "More..", image: UIImage(systemName: "ellipsis"), handler: { action in
guard let cell = self.conversationLogController?.tableView.cellForRow(at: indexPath) else {
return;
}
NotificationCenter.default.post(name: Notification.Name("tableViewCellShowEditToolbar"), object: cell);
})
])
return UIMenu(title: "", children: items);
}
}

View file

@ -142,7 +142,7 @@ class ChatViewController : BaseChatViewControllerWithDataSourceAndContextMenuAnd
case let item as ChatMessage:
let id = continuation ? "ChatTableViewMessageContinuationCell" : "ChatTableViewMessageCell";
let cell: ChatTableViewCell = tableView.dequeueReusableCell(withIdentifier: id, for: indexPath) as! ChatTableViewCell;
cell.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.contentView.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
let name = incoming ? self.titleView.name : "Me";
cell.avatarView?.set(name: name, avatar: AvatarManager.instance.avatar(for: incoming ? jid : account, on: account), orDefault: AvatarManager.instance.defaultAvatar);
cell.nicknameView?.text = name;
@ -154,7 +154,7 @@ class ChatViewController : BaseChatViewControllerWithDataSourceAndContextMenuAnd
case let item as ChatAttachment:
let id = continuation ? "ChatTableViewAttachmentContinuationCell" : "ChatTableViewAttachmentCell" ;
let cell: AttachmentChatTableViewCell = tableView.dequeueReusableCell(withIdentifier: id, for: indexPath) as! AttachmentChatTableViewCell;
cell.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.contentView.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
let name = incoming ? self.titleView.name : "Me";
cell.avatarView?.set(name: name, avatar: AvatarManager.instance.avatar(for: incoming ? jid : account, on: account), orDefault: AvatarManager.instance.defaultAvatar);
cell.nicknameView?.text = name;
@ -166,18 +166,18 @@ class ChatViewController : BaseChatViewControllerWithDataSourceAndContextMenuAnd
case let item as ChatLinkPreview:
let id = "ChatTableViewLinkPreviewCell";
let cell: LinkPreviewChatTableViewCell = tableView.dequeueReusableCell(withIdentifier: id, for: indexPath) as! LinkPreviewChatTableViewCell;
cell.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.contentView.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.set(linkPreview: item);
return cell;
case let item as SystemMessage:
let cell: ChatTableViewSystemCell = tableView.dequeueReusableCell(withIdentifier: "ChatTableViewSystemCell", for: indexPath) as! ChatTableViewSystemCell;
cell.set(item: item);
cell.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.contentView.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
return cell;
case let item as ChatInvitation:
let id = "ChatTableViewInvitationCell";
let cell: InvitationChatTableViewCell = tableView.dequeueReusableCell(withIdentifier: id, for: indexPath) as! InvitationChatTableViewCell;
cell.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.contentView.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
let name = incoming ? self.titleView.name : "Me";
cell.avatarView?.set(name: name, avatar: AvatarManager.instance.avatar(for: incoming ? jid : account, on: account), orDefault: AvatarManager.instance.defaultAvatar);
cell.nicknameView?.text = name;
@ -380,7 +380,7 @@ class ChatViewController : BaseChatViewControllerWithDataSourceAndContextMenuAnd
return;
}
MessageEventHandler.sendMessage(chat: self.chat as! DBChat, body: text, url: nil);
MessageEventHandler.sendMessage(chat: self.chat as! DBChat, body: text, url: nil, correctedMessageOriginId: self.correctedMessageOriginId);
DispatchQueue.main.async {
self.messageText = nil;
}

View file

@ -253,6 +253,15 @@ class ChatViewDataSource {
}
}
func isAnyMatching(_ fn: (ChatViewItemProtocol)->Bool, in range: Range<Int>) -> Bool {
for i in range {
if let item = store.item(at: i), fn(item) {
return true;
}
}
return false;
}
func refreshDataNoReload() {
queue.async {
let store = DispatchQueue.main.sync { return self.store; };

View file

@ -33,12 +33,12 @@ class LinkPreviewChatTableViewCell: BaseChatTableViewCell {
value.removeFromSuperview();
}
if let value = linkView {
self.addSubview(value);
self.contentView.addSubview(value);
NSLayoutConstraint.activate([
value.topAnchor.constraint(equalTo: self.topAnchor, constant: 2),
value.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -4),
value.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 44),
value.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor, constant: -22)
value.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 2),
value.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -4),
value.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 44),
value.trailingAnchor.constraint(lessThanOrEqualTo: self.contentView.trailingAnchor, constant: -22)
]);
}
}

View file

@ -33,14 +33,14 @@ public class DBChatHistoryStore {
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)");
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, master_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, :master_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 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 findItemByOriginId: DBStatement = try! DBConnection.main.prepareStatement("SELECT id FROM chat_history WHERE account = :account AND jid = :jid AND (stanza_id = :stanza_id OR correction_stanza_id = :stanza_id) AND (:author_nickname IS NULL OR author_nickname = :author_nickname) AND (:participant_id IS NULL OR participant_id = :participant_id) ORDER BY timestamp DESC");
fileprivate let updateServerMsgId: DBStatement = try! DBConnection.main.prepareStatement("UPDATE chat_history SET server_msg_id = :server_msg_id WHERE id = :id AND server_msg_id is null");
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))");
@ -51,7 +51,7 @@ public class DBChatHistoryStore {
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 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, correction_stanza_id 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)")
@ -64,6 +64,8 @@ public class DBChatHistoryStore {
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");
private let correctLastMessageStmt: DBStatement = try! DBConnection.main.prepareStatement("UPDATE chat_history SET data = :data, state = :state, correction_stanza_id = :correction_stanza_id, correction_timestamp = :correction_timestamp, remote_msg_id = :remote_msg_id, server_msg_id = COALESCE(:server_msg_id, server_msg_id) WHERE id = :id AND (correction_stanza_id IS NULL OR correction_stanza_id <> :correction_stanza_id) AND (correction_timestamp IS NULL OR correction_timestamp < :correction_timestamp)");
fileprivate let dispatcher: QueueDispatcher;
static func convertToAttachments() {
@ -107,12 +109,12 @@ public class DBChatHistoryStore {
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, appendix: appendix, linkPreviewAction: .none, completionHandler: { newId in
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, appendix: appendix, linkPreviewAction: .none, masterId: nil, 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
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, masterId: nil, completionHandler: { newId in
_ = try! removePreviewStmt.update(item.id);
});
} else {
@ -130,7 +132,7 @@ public class DBChatHistoryStore {
for (url, _) 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
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, masterId: nil, completionHandler: { newId in
group.leave();
});
}
@ -216,12 +218,7 @@ public class DBChatHistoryStore {
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);
@ -239,7 +236,7 @@ public class DBChatHistoryStore {
return;
}
if self.findItemId(for: account, with: jid, serverMsgId: serverMsgId, originId: originId, timestamp: timestamp, direction: state.direction, itemType: itemType, stanzaId: message.id, authorNickname: authorNickname, data: body) != nil {
if let originId = stanzaId, let correctedMessageId = message.lastMessageCorrectionId, self.correctMessageSync(for: account, with: jid, stanzaId: correctedMessageId, authorNickname: authorNickname, participantId: participantId, data: body, correctionStanzaId: originId, correctionTimestamp: timestamp, serverMsgId: serverMsgId, remoteMsgId: remoteMsgId, newState: state) {
// this message was already added to the store..
// should this be here...?
if let chatState = message.chatState {
@ -248,7 +245,18 @@ public class DBChatHistoryStore {
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, appendix: appendix, linkPreviewAction: .auto, completionHandler: nil);
if let stableId = serverMsgId, let existingMessageId = self.findItemId(for: account, serverMsgId: stableId) {
return;
}
if let originId = stanzaId, let existingMessageId = self.findItemId(for: account, with: jid, originId: originId, authorNickname: authorNickname, participantId: participantId) {
if let stableId = serverMsgId {
_ = try! self.updateServerMsgId.update(["id": existingMessageId, "server_msg_id": serverMsgId] as [String: Any?]);
}
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, appendix: appendix, linkPreviewAction: .auto, masterId: nil, completionHandler: nil);
}
}
@ -258,51 +266,30 @@ public class DBChatHistoryStore {
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;
}
}
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);
private func findItemId(for account: BareJID, serverMsgId: String) -> Int? {
return try! self.findItemByServerMsgId.findFirst(["server_msg_id": serverMsgId, "account": account] as [String: Any?], map: { cursor -> Int? in
return cursor["id"];
});
}
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 findItemId(for account: BareJID, with jid: BareJID, originId: String, authorNickname: String?, participantId: String?) -> Int? {
return try! self.findItemByOriginId.findFirst(["stanza_id": originId, "account": account, "jid": jid, "author_nickname": authorNickname, "participant_id": participantId] as [String: Any?], map: { cursor -> Int? in
return cursor["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?, appendix: AppendixProtocol?, linkPreviewAction: LinkPreviewAction, completionHandler: ((Int) -> Void)?) {
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?, appendix: AppendixProtocol?, linkPreviewAction: LinkPreviewAction, masterId: Int? = nil, completionHandler: ((Int) -> Void)?) {
var item: ChatViewItemProtocol?;
if linkPreviewAction != .only {
let appendixStr: String? = appendix?.string(encoding: .utf8);
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": appendixStr, "server_msg_id": serverMsgId, "remote_msg_id": remoteMsgId];
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": appendixStr, "server_msg_id": serverMsgId, "remote_msg_id": remoteMsgId, "master_id": masterId];
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, participantId: participantId, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: errorMessage);
@ -321,35 +308,115 @@ public class DBChatHistoryStore {
}
}
}
if linkPreviewAction != .none && type == .message, #available(iOS 13.0, *) {
// if we may have previews, we should add them here..
// how about using separate queue just to improve processing of data..
DispatchQueue.global(qos: .background).async {
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) {
if (data as NSString).range(of: "http", options: .caseInsensitive, range: match.range).location == match.range.location {
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);
}
}
}
}
if linkPreviewAction != .none && type == .message, let id = item?.id {
self.generatePreviews(forItem: id, account: account, jid: jid, state: state, authorNickname: authorNickname, authorJid: authorJid, recipientNickname: recipientNickname, participantId: participantId, timestamp: timestamp, data: data);
}
}
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?, appendix: AppendixProtocol? = nil, linkPreviewAction: LinkPreviewAction, completionHandler: ((Int) -> Void)?) {
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?, appendix: AppendixProtocol? = nil, linkPreviewAction: LinkPreviewAction, masterId: Int? = nil, 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, appendix: appendix, linkPreviewAction: linkPreviewAction, completionHandler: completionHandler);
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, appendix: appendix, linkPreviewAction: linkPreviewAction, masterId: masterId, completionHandler: completionHandler);
}
}
open func correctMessage(for account: BareJID, with jid: BareJID, stanzaId: String, authorNickname: String?, participantId: String?, data: String, correctionStanzaId: String?, correctionTimestamp: Date, newState: MessageState) {
let timestamp = Date(timeIntervalSince1970: Double(Int64((correctionTimestamp).timeIntervalSince1970 * 1000)) / 1000);
dispatcher.async {
_ = self.correctMessageSync(for: account, with: jid, stanzaId: stanzaId, authorNickname: authorNickname, participantId: participantId, data: data, correctionStanzaId: correctionStanzaId, correctionTimestamp: timestamp, serverMsgId: nil, remoteMsgId: nil, newState: newState);
}
}
private func correctMessageSync(for account: BareJID, with jid: BareJID, stanzaId: String, authorNickname: String?, participantId: String?, data: String, correctionStanzaId: String?, correctionTimestamp: Date, serverMsgId: String?, remoteMsgId: String?, newState: MessageState) -> Bool {
// we need to check participant-id/sender nickname to make it work correctly
// moreover, stanza-id should be checked with origin-id for MUC/MIX (not message id)
// MIX/MUC should send origin-id if they assume to use last message correction!
if let itemId = self.findItemId(for: account, with: jid, originId: stanzaId, authorNickname: authorNickname, participantId: participantId) {
if let oldItem: ChatViewItemProtocol = try! self.getChatMessageWithIdStmt.findFirst(["id": itemId] as [String: Any?], map: {
return self.itemFrom(cursor: $0, for: account, with: jid)
}) {
let params: [String: Any?] = ["id": itemId, "data": data, "state": newState.rawValue, "correction_stanza_id": correctionStanzaId, "remote_msg_id": remoteMsgId, "server_msg_id": serverMsgId, "correction_timestamp": correctionTimestamp];
let updated = try! self.correctLastMessageStmt.update(params);
if updated > 0 {
let newMessageState: MessageState = (oldItem.state.direction == .incoming) ? (oldItem.state.isUnread ? .incoming : (newState.isUnread ? .incoming_unread : .incoming)) : (.outgoing);
DBChatStore.instance.newMessage(for: account, with: jid, timestamp: oldItem.timestamp, itemType: .message, message: data, state: newMessageState, completionHandler: {
print("chat store state updated with message state:", newMessageState.rawValue, "old state:", oldItem.state.rawValue, "new state:", newState.rawValue);
})
print("correcing previews for master id:", itemId);
self.itemUpdated(withId: itemId, for: account, with: jid);
self.previewGenerationDispatcher.async(flags: .barrier, execute: {
self.dispatcher.sync {
print("removing previews for master id:", itemId);
self.removePreviews(idOfRelatedToItem: itemId);
if newState != .outgoing_unsent {
self.generatePreviews(forItem: itemId, account: account, jid: jid, state: newState);
}
}
})
}
}
return true;
} else {
return false;
}
}
private func generatePreviews(forItem masterId: Int, account: BareJID, jid: BareJID, state: MessageState) {
if #available(iOS 13.0, *) {
let params: [String: Any?] = ["id": masterId];
guard let item = try! self.getChatMessageWithIdStmt.findFirst(params, map: { (cursor) in
return self.itemFrom(cursor: cursor, for: account, with: jid) as? ChatMessage
}) else {
return;
}
self.generatePreviews(forItem: item.id, account: item.account, jid: item.jid, state: item.state, authorNickname: item.authorNickname, authorJid: item.authorJid, recipientNickname: item.recipientNickname, participantId: item.participantId, timestamp: item.timestamp, data: item.message);
}
}
private var previewsInProgress: [Int: UUID] = [:];
private let previewGenerationDispatcher = QueueDispatcher(label: "chat_history_store", attributes: [.concurrent]);
private func generatePreviews(forItem masterId: Int, account: BareJID, jid: BareJID, state messageState: MessageState, authorNickname: String?, authorJid: BareJID?, recipientNickname: String?, participantId: String?, timestamp: Date, data: String) {
if #available(iOS 13.0, *) {
let state = messageState == .incoming_unread ? .incoming : messageState;
let uuid = UUID();
previewsInProgress[masterId] = uuid;
previewGenerationDispatcher.async {
print("generating previews for master id:", masterId, "uuid:", uuid);
// 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));
guard self.dispatcher.sync(execute: {
let valid = self.previewsInProgress[masterId] == uuid;
if valid {
self.previewsInProgress.removeValue(forKey: masterId);
}
return valid;
}) else {
return;
}
print("adding previews for master id:", masterId, "uuid:", uuid);
matches.forEach { match in
if let url = match.url, let scheme = url.scheme, ["https", "http"].contains(scheme) {
if (data as NSString).range(of: "http", options: .caseInsensitive, range: match.range).location == match.range.location {
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, masterId: masterId, 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, masterId: masterId, completionHandler: nil);
}
}
}
}
}
}
open func removeHistory(for account: BareJID, with jid: BareJID?) {
dispatcher.async {
let params: [String: Any?] = ["account": account, "jid": jid];
@ -388,9 +455,8 @@ public class DBChatHistoryStore {
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];
if let msgId = self.findItemId(for: account, with: jid, originId: id, authorNickname: nil, participantId: nil) {
let params: [String: Any?] = ["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);
@ -427,8 +493,7 @@ public class DBChatHistoryStore {
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);
return self.findItemId(for: account, with: jid, originId: stanzaId, authorNickname: nil, participantId: nil);
}
}
@ -456,6 +521,9 @@ public class DBChatHistoryStore {
return;
}
self.itemUpdated(withId: msgId, for: account, with: jid);
if oldState == .outgoing_unsent && newState != .outgoing_unsent {
self.generatePreviews(forItem: msgId, account: account, jid: jid, state: newState);
}
}
}
@ -469,28 +537,41 @@ public class DBChatHistoryStore {
}
self.itemRemoved(withId: item.id, for: item.account, with: item.jid);
if #available(iOS 13.0, *), let item = item as? ChatMessage {
if self.findLinkPreviewsForMessageStmt == nil {
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");
self.removePreviews(idOfRelatedToItem: item.id);
}
}
private func removePreviews(idOfRelatedToItem masterId: Int) {
if #available(iOS 13.0, *) {
if self.findLinkPreviewsForMessageStmt == nil {
self.findLinkPreviewsForMessageStmt = try! DBConnection.main.prepareStatement("SELECT id, account, jid, data FROM chat_history WHERE master_id = :master_id AND item_type = \(ItemType.linkPreview.rawValue)");
}
// for chat message we might have a link previews which we need to remove..
let linkParams: [String: Any?] = ["master_id": masterId];
guard let linkPreviews = try? self.findLinkPreviewsForMessageStmt?.query(linkParams, map: { cursor -> (Int, BareJID, BareJID)? in
guard let id: Int = cursor["id"], let account: BareJID = cursor["account"], let jid: BareJID = cursor["jid"] else {
return nil;
}
// 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];
guard let linkPreviews = try? self.findLinkPreviewsForMessageStmt?.query(linkParams, map: { cursor -> (Int, String)? in
guard let id: Int = cursor["id"], let url: String = cursor["data"] else {
return nil;
}
return (id, url);
}), !linkPreviews.isEmpty else {
return;
return (id, account, jid);
}), !linkPreviews.isEmpty else {
return;
}
for (id, account, jid) in linkPreviews {
// this is a preview and needs to be removed..
let removeLinkParams: [String: Any?] = ["id": id];
if (try! self.removeItemStmt.update(removeLinkParams)) > 0 {
self.itemRemoved(withId: id, for: account, with: jid);
}
for (id, url) in linkPreviews {
if item.message.contains(url) {
// this is a preview and needs to be removed..
let removeLinkParams: [String: Any?] = ["id": id];
if (try! self.removeItemStmt.update(removeLinkParams)) > 0 {
self.itemRemoved(withId: id, for: item.account, with: item.jid);
}
}
}
}
}
func originId(for account: BareJID, with jid: BareJID, id: Int, completionHandler: @escaping (String)->Void ){
dispatcher.async {
let stmt = try! DBConnection.main.prepareStatement("select stanza_id from chat_history where id = ?");
if let stanzaId: String = try! stmt.findFirst(id, map: { $0["stanza_id"] }) {
DispatchQueue.main.async {
completionHandler(stanzaId);
}
}
}
@ -536,7 +617,7 @@ public class DBChatHistoryStore {
}
}
func loadUnsentMessage(for account: BareJID, completionHandler: @escaping (BareJID,BareJID,String,String,MessageEncryption, ItemType)->Void) {
func loadUnsentMessage(for account: BareJID, completionHandler: @escaping (BareJID,BareJID,String,String,MessageEncryption,String?,ItemType)->Void) {
dispatcher.async {
try! self.getUnsentMessagesForAccountStmt.query(["account": account] as [String : Any?], forEach: { (cursor) in
let jid: BareJID = cursor["jid"]!;
@ -544,8 +625,9 @@ public class DBChatHistoryStore {
let data: String = cursor["data"]!;
let stanzaId: String = cursor["stanza_id"]!;
let encryption: MessageEncryption = MessageEncryption(rawValue: cursor["encryption"] ?? 0) ?? .none;
let correctionStanzaId: String? = cursor["correction_stanza_id"];
completionHandler(account, jid, data, stanzaId, encryption, type);
completionHandler(account, jid, data, stanzaId, encryption, correctionStanzaId, type);
});
}
}

View file

@ -333,8 +333,9 @@ open class DBChatStore {
dispatcher.async {
if let chat = self.getChat(for: account, with: jid) {
let lastActivity = LastChatActivity.from(itemType: itemType, data: message, sender: senderNickname);
if chat.updateLastActivity(lastActivity, timestamp: timestamp, isUnread: state.isUnread) {
if state.isUnread && !self.isMuted(chat: chat) {
let unread = lastActivity != nil && state.isUnread;
if chat.updateLastActivity(lastActivity, timestamp: timestamp, isUnread: unread) {
if unread && !self.isMuted(chat: chat) {
self.unreadMessagesCount = self.unreadMessagesCount + 1;
}
if remoteChatState != nil {
@ -711,7 +712,7 @@ class DBChat: Chat, DBChatProtocol {
if isUnread {
unread = unread + 1;
}
guard self.lastActivity == nil || self.timestamp.compare(timestamp) == .orderedAscending else {
guard self.lastActivity == nil || self.timestamp.compare(timestamp) != .orderedDescending else {
return isUnread;
}
if lastActivity != nil {
@ -822,7 +823,7 @@ class DBRoom: Room, DBChatProtocol {
if isUnread {
unread = unread + 1;
}
guard self.lastActivity == nil || self.timestamp.compare(timestamp) == .orderedAscending else {
guard self.lastActivity == nil || self.timestamp.compare(timestamp) != .orderedDescending else {
return isUnread;
}
@ -850,6 +851,14 @@ class DBRoom: Room, DBChatProtocol {
}
}
override func createMessage(_ body: String?) -> Message {
let message = super.createMessage(body);
if message.id == nil {
message.id = UUID().uuidString;
}
return message;
}
override func createPrivateMessage(_ body: String?, recipientNickname: String) -> Message {
let stanza = super.createPrivateMessage(body, recipientNickname: recipientNickname);
let id = UUID().uuidString;
@ -898,7 +907,7 @@ class DBChannel: Channel, DBChatProtocol {
if isUnread {
unread = unread + 1;
}
guard self.lastActivity == nil || self.timestamp.compare(timestamp) == .orderedAscending else {
guard self.lastActivity == nil || self.timestamp.compare(timestamp) != .orderedDescending else {
return isUnread;
}

View file

@ -105,7 +105,7 @@ class MucChatViewController: BaseChatViewControllerWithDataSourceAndContextMenuA
let id = continuation ? "ChatTableViewMessageContinuationCell" : "ChatTableViewMessageCell";
let cell = tableView.dequeueReusableCell(withIdentifier: id, for: indexPath) as! ChatTableViewCell;
cell.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.contentView.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
// cell.nicknameLabel?.text = item.nickname;
if cell.avatarView != nil {
if let senderJid = item.state.direction == .incoming ? item.authorJid : item.account {
@ -136,7 +136,7 @@ class MucChatViewController: BaseChatViewControllerWithDataSourceAndContextMenuA
case let item as ChatAttachment:
let id = continuation ? "ChatTableViewAttachmentContinuationCell" : "ChatTableViewAttachmentCell";
let cell: AttachmentChatTableViewCell = tableView.dequeueReusableCell(withIdentifier: id, for: indexPath) as! AttachmentChatTableViewCell;
cell.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.contentView.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
if cell.avatarView != nil {
if let senderJid = item.state.direction == .incoming ? item.authorJid : item.account {
cell.avatarView?.set(name: item.authorNickname, avatar: AvatarManager.instance.avatar(for: senderJid, on: item.account), orDefault: AvatarManager.instance.defaultAvatar);
@ -169,18 +169,18 @@ class MucChatViewController: BaseChatViewControllerWithDataSourceAndContextMenuA
case let item as ChatLinkPreview:
let id = "ChatTableViewLinkPreviewCell";
let cell: LinkPreviewChatTableViewCell = tableView.dequeueReusableCell(withIdentifier: id, for: indexPath) as! LinkPreviewChatTableViewCell;
cell.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.contentView.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.set(linkPreview: item);
return cell;
case let item as SystemMessage:
let cell: ChatTableViewSystemCell = tableView.dequeueReusableCell(withIdentifier: "ChatTableViewSystemCell", for: indexPath) as! ChatTableViewSystemCell;
cell.set(item: item);
cell.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.contentView.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
return cell;
case let item as ChatInvitation:
let id = "ChatTableViewInvitationCell";
let cell: InvitationChatTableViewCell = tableView.dequeueReusableCell(withIdentifier: id, for: indexPath) as! InvitationChatTableViewCell;
cell.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
cell.contentView.transform = dataSource.inverted ? CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) : CGAffineTransform.identity;
if cell.avatarView != nil {
if let senderJid = item.state.direction == .incoming ? item.authorJid : item.account {
cell.avatarView?.set(name: item.authorNickname, avatar: AvatarManager.instance.avatar(for: senderJid, on: item.account), orDefault: AvatarManager.instance.defaultAvatar);
@ -287,7 +287,9 @@ class MucChatViewController: BaseChatViewControllerWithDataSourceAndContextMenuA
return;
}
self.room!.sendMessage(text, url: nil, additionalElements: []);
let msg = self.room!.createMessage(text);
msg.lastMessageCorrectionId = self.correctedMessageOriginId;
XmppService.instance.getClient(for: account)?.context.writer?.write(msg);
DispatchQueue.main.async {
self.messageText = nil;
}

View file

@ -145,7 +145,7 @@ class MessageEventHandler: XmppServiceEventHandler {
})
}
static func sendMessage(chat: DBChat, body: String?, url: String?, encrypted: ChatEncryption? = nil, stanzaId: String? = nil, chatAttachmentAppendix: ChatAttachmentAppendix? = nil, messageStored: ((Int)->Void)? = nil) {
static func sendMessage(chat: DBChat, body: String?, url: String?, encrypted: ChatEncryption? = nil, stanzaId: String? = nil, chatAttachmentAppendix: ChatAttachmentAppendix? = nil, correctedMessageOriginId: String? = nil, messageStored: ((Int)->Void)? = nil) {
guard let msg = body ?? url else {
return;
}
@ -155,6 +155,7 @@ class MessageEventHandler: XmppServiceEventHandler {
let message = chat.createMessage(msg);
message.id = stanzaId ?? UUID().uuidString;
message.messageDelivery = .request;
message.lastMessageCorrectionId = correctedMessageOriginId;
let account = chat.account;
let jid = chat.jid.bareJid;
@ -162,16 +163,18 @@ class MessageEventHandler: XmppServiceEventHandler {
switch encryption {
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, 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, appendix: chatAttachmentAppendix, linkPreviewAction: .none, completionHandler: messageStored);
if let correctedMessageId = correctedMessageOriginId {
DBChatHistoryStore.instance.correctMessage(for: account, with: jid, stanzaId: correctedMessageId, authorNickname: nil, participantId: nil, data: msg, correctionStanzaId: message.id!, correctionTimestamp: Date(), newState: .outgoing_unsent);
} else {
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, 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, appendix: 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(_):
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, appendix: chatAttachmentAppendix, linkPreviewAction: .only, completionHandler: messageStored);
DBChatHistoryStore.instance.updateItemState(for: account, with: jid, stanzaId: correctedMessageOriginId ?? message.id!, from: .outgoing_unsent, to: .outgoing, withTimestamp: correctedMessageOriginId != nil ? nil : Date());
case .failure(let err):
let condition = (err is ErrorCondition) ? (err as? ErrorCondition) : nil;
guard condition == nil || condition! != .gone else {
@ -198,15 +201,17 @@ 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, 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, appendix: chatAttachmentAppendix, linkPreviewAction: .none, completionHandler: messageStored);
if let correctedMessageId = correctedMessageOriginId {
DBChatHistoryStore.instance.correctMessage(for: account, with: jid, stanzaId: correctedMessageId, authorNickname: nil, participantId: nil, data: msg, correctionStanzaId: message.id!, correctionTimestamp: Date(), newState: .outgoing_unsent);
} else {
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, appendix: chatAttachmentAppendix, linkPreviewAction: .none, completionHandler: messageStored);
}
}
XmppService.instance.tasksQueue.schedule(for: jid, task: { (completionHandler) in
sendUnencryptedMessage(message, from: account, completionHandler: { result in
switch result {
case .success(_):
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: type, timestamp: timestamp, stanzaId: nil, serverMsgId: nil, remoteMsgId: nil, data: msg, encryption: .none, encryptionFingerprint: nil, appendix: chatAttachmentAppendix, linkPreviewAction: .only, completionHandler: nil);
DBChatHistoryStore.instance.updateItemState(for: account, with: jid, stanzaId: correctedMessageOriginId ?? message.id!, from: .outgoing_unsent, to: .outgoing, withTimestamp: correctedMessageOriginId != nil ? nil : Date());
case .failure(let err):
guard let condition = err as? ErrorCondition, condition != .gone else {
completionHandler();
@ -264,7 +269,7 @@ class MessageEventHandler: XmppServiceEventHandler {
}
fileprivate func sendUnsentMessages(for account: BareJID) {
DBChatHistoryStore.instance.loadUnsentMessage(for: account, completionHandler: { (account, jid, data, stanzaId, encryption, type) in
DBChatHistoryStore.instance.loadUnsentMessage(for: account, completionHandler: { (account, jid, data, stanzaId, encryption, correctionStanzaId, type) in
var chat = DBChatStore.instance.getChat(for: account, with: jid);
if chat == nil {
@ -278,7 +283,7 @@ class MessageEventHandler: XmppServiceEventHandler {
if let dbChat = chat as? DBChat {
if type == .message {
MessageEventHandler.sendMessage(chat: dbChat, body: data, url: nil, stanzaId: stanzaId);
MessageEventHandler.sendMessage(chat: dbChat, body: data, url: nil, stanzaId: correctionStanzaId == nil ? stanzaId : correctionStanzaId, correctedMessageOriginId: correctionStanzaId == nil ? nil : stanzaId);
} else if type == .attachment {
MessageEventHandler.sendMessage(chat: dbChat, body: data, url: data, stanzaId: stanzaId);
}