Improved file sharing and added link previews #siskinim-178
This commit is contained in:
parent
f21170e44f
commit
8011bae7cb
|
@ -25,7 +25,7 @@ import TigaseSwift
|
|||
|
||||
public class DBSchemaManager {
|
||||
|
||||
static let CURRENT_VERSION = 8;
|
||||
static let CURRENT_VERSION = 9;
|
||||
|
||||
fileprivate let dbConnection: DBConnection;
|
||||
|
||||
|
|
7
Shared/db-schema-9.sql
Normal file
7
Shared/db-schema-9.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
BEGIN;
|
||||
|
||||
ALTER TABLE chat_history ADD COLUMN appendix TEXT;
|
||||
|
||||
COMMIT;
|
||||
|
||||
PRAGMA user_version = 9;
|
|
@ -106,29 +106,60 @@ class ShareViewController: SLComposeServiceViewController {
|
|||
|
||||
override func didSelectPost() {
|
||||
if let provider = (self.extensionContext!.inputItems.first as? NSExtensionItem)?.attachments?.first {
|
||||
if provider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
|
||||
provider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil, completionHandler: { (value, error) in
|
||||
self.shareText(url: value as! URL);
|
||||
})
|
||||
} else {
|
||||
if let type = [kUTTypeFileURL, kUTTypeImage, kUTTypeMovie].filter({ (it) -> Bool in
|
||||
return provider.hasItemConformingToTypeIdentifier(it as String);
|
||||
}).first {
|
||||
provider.loadItem(forTypeIdentifier: type as String, options: nil, completionHandler: { (item, error) in
|
||||
if let localUrl = item as? URL {
|
||||
let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, localUrl.pathExtension as CFString, nil)?.takeRetainedValue();
|
||||
self.upload(localUrl: localUrl, type: uti != nil ? (UTTypeCopyPreferredTagWithClass(uti!, kUTTagClassMIMEType)?.takeRetainedValue() as String?) : nil, handler: {(remoteUrl) in
|
||||
guard remoteUrl != nil else {
|
||||
self.showAlert(title: "Failure", message: "Please try again later.");
|
||||
return;
|
||||
if provider.hasItemConformingToTypeIdentifier(kUTTypeFileURL as String) {
|
||||
provider.loadItem(forTypeIdentifier: kUTTypeFileURL as String, options: nil, completionHandler: { (item, error) in
|
||||
if let localUrl = item as? URL {
|
||||
let uti = try? localUrl.resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier;
|
||||
let mimeType = uti != nil ? (UTTypeCopyPreferredTagWithClass(uti! as CFString, kUTTagClassMIMEType)?.takeRetainedValue() as String?) : nil;
|
||||
let size = try? FileManager.default.attributesOfItem(atPath: localUrl.path)[FileAttributeKey.size] as? UInt64;
|
||||
self.upload(localUrl: localUrl, type: mimeType, handler: {(remoteUrl) in
|
||||
guard remoteUrl != nil else {
|
||||
self.showAlert(title: "Failure", message: "Please try again later.");
|
||||
return;
|
||||
}
|
||||
|
||||
if self.sharedDefaults!.integer(forKey: "fileDownloadSizeLimit") > 0 {
|
||||
let hash = Digest.sha1.digest(toHex: remoteUrl!.absoluteString.data(using: .utf8)!)!;
|
||||
|
||||
var params: [String: Any] = [
|
||||
"jids": self.recipients.map({ $0.bareJid.stringValue }),
|
||||
"name": localUrl.lastPathComponent,
|
||||
"timestamp": Date()
|
||||
];
|
||||
if mimeType != nil {
|
||||
params["mimeType"] = mimeType;
|
||||
}
|
||||
self.shareText(url: remoteUrl!);
|
||||
});
|
||||
} else {
|
||||
self.showAlert(title: "Failure", message: "Please try again later.");
|
||||
}
|
||||
})
|
||||
}
|
||||
if size != nil {
|
||||
params["size"] = Int(size!);
|
||||
}
|
||||
|
||||
let localUploadDirUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.siskinim.shared")!.appendingPathComponent("upload", isDirectory: true);
|
||||
if !FileManager.default.fileExists(atPath: localUploadDirUrl.path) {
|
||||
try? FileManager.default.createDirectory(at: localUploadDirUrl, withIntermediateDirectories: true, attributes: nil);
|
||||
}
|
||||
do {
|
||||
try FileManager.default.copyItem(at: localUrl, to: localUploadDirUrl.appendingPathComponent(hash, isDirectory: false));
|
||||
self.sharedDefaults!.set(params as Any?, forKey: "upload-\(hash)");
|
||||
} catch {
|
||||
print("could not copy a file from:", localUrl, "to:", localUploadDirUrl)
|
||||
}
|
||||
}
|
||||
self.share(url: nil, uploadedFileURL: remoteUrl);
|
||||
});
|
||||
} else {
|
||||
self.showAlert(title: "Failure", message: "Please try again later.");
|
||||
}
|
||||
})
|
||||
} else if provider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
|
||||
provider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil, completionHandler: { (value, error) in
|
||||
self.share(url: value as! URL, uploadedFileURL: nil);
|
||||
})
|
||||
// } else if provider.hasItemConformingToTypeIdentifier(kUTTypePlainText as String) {
|
||||
// provider.loadItem(forTypeIdentifier: kUTTypePlainText as String, options: nil, completionHandler: { (item, error) in
|
||||
// self.share(text: item as! String);
|
||||
// });
|
||||
// } else {
|
||||
// self.showAlert(title: "Failure", message: "Please try again later.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -218,7 +249,7 @@ class ShareViewController: SLComposeServiceViewController {
|
|||
return nil;
|
||||
}
|
||||
|
||||
func upload(localUrl: URL, type: String?, handler: (URL?)->Void) {
|
||||
func upload(localUrl: URL, type: String?, handler: @escaping (URL?)->Void) {
|
||||
let size = try! FileManager.default.attributesOfItem(atPath: localUrl.path)[FileAttributeKey.size] as! UInt64;
|
||||
print("trying to upload", localUrl, "size", size, "type", type as Any);
|
||||
if let httpModule: HttpFileUploadModule = self.xmppClient.modulesManager.getModule(HttpFileUploadModule.ID) {
|
||||
|
@ -254,7 +285,7 @@ class ShareViewController: SLComposeServiceViewController {
|
|||
self.showAlert(title: "Upload failed", message: "Upload to HTTP server failed.");
|
||||
return;
|
||||
}
|
||||
self.shareText(url: slot.getUri);
|
||||
handler(slot.getUri);
|
||||
}.resume();
|
||||
}, onError: {(errorCondition, message) in
|
||||
self.showAlert(title: "Upload failed", message: message ?? "Please try again later.");
|
||||
|
@ -271,24 +302,59 @@ class ShareViewController: SLComposeServiceViewController {
|
|||
}
|
||||
}
|
||||
|
||||
func shareText(url: URL) {
|
||||
print("sharing", contentText as Any, url);
|
||||
|
||||
func share(text: String? = nil, url: URL? = nil, uploadedFileURL: URL? = nil) {
|
||||
recipients.forEach { (recipient) in
|
||||
let message = Message();
|
||||
message.type = StanzaType.chat;
|
||||
message.to = recipient;
|
||||
message.body = contentText.isEmpty ? url.description : "\(contentText!) - \(url.description)";
|
||||
message.oob = url.description;
|
||||
xmppClient.context.writer?.write(message);
|
||||
if !contentText.isEmpty || url != nil {
|
||||
let message = Message();
|
||||
message.type = StanzaType.chat;
|
||||
message.to = recipient;
|
||||
|
||||
if let text = text {
|
||||
message.body = contentText.isEmpty ? text : "\(contentText!) - \(text)";
|
||||
} else if let url = url {
|
||||
message.body = contentText.isEmpty ? url.description : "\(contentText!) - \(url.description)";
|
||||
} else {
|
||||
message.body = contentText;
|
||||
}
|
||||
xmppClient.context.writer?.write(message);
|
||||
}
|
||||
|
||||
if let url = uploadedFileURL {
|
||||
let message = Message();
|
||||
message.type = .chat;
|
||||
message.to = recipient;
|
||||
message.oob = url.description;
|
||||
xmppClient.context.writer?.write(message);
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: {
|
||||
self.xmppClient.disconnect();
|
||||
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// func shareText(url: URL?) {
|
||||
// print("sharing", contentText as Any, url);
|
||||
//
|
||||
// recipients.forEach { (recipient) in
|
||||
// let message = Message();
|
||||
// message.type = StanzaType.chat;
|
||||
// message.to = recipient;
|
||||
// if let url = url {
|
||||
// message.body = contentText.isEmpty ? url.description : "\(contentText!) - \(url.description)";
|
||||
// message.oob = url.description;
|
||||
// }
|
||||
// xmppClient.context.writer?.write(message);
|
||||
// }
|
||||
//
|
||||
// DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: {
|
||||
// self.xmppClient.disconnect();
|
||||
// self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil);
|
||||
// });
|
||||
// }
|
||||
|
||||
class ShareEventHandler: EventHandler {
|
||||
|
||||
weak var controller: ShareViewController?;
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
FE4071E421E2605900F09B58 /* VideoCallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4071E321E2605900F09B58 /* VideoCallController.swift */; };
|
||||
FE4071E621E262D900F09B58 /* VoIP.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FE4071E521E262D900F09B58 /* VoIP.storyboard */; };
|
||||
FE4071E821E2653700F09B58 /* RoundButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4071E721E2653700F09B58 /* RoundButton.swift */; };
|
||||
FE43E43823BF3DE80079BD9B /* ChatAttachementsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE43E43723BF3DE80079BD9B /* ChatAttachementsController.swift */; };
|
||||
FE43EB551F3CC55900A4CAAD /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE43EB541F3CC55900A4CAAD /* ImageCache.swift */; };
|
||||
FE43EB571F3DBAAE00A4CAAD /* BaseChatViewController_PreviewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE43EB561F3DBAAE00A4CAAD /* BaseChatViewController_PreviewExtension.swift */; };
|
||||
FE4DDF561F39E0B500A4CE5A /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4DDF551F39E0B500A4CE5A /* ShareViewController.swift */; };
|
||||
|
@ -106,6 +107,8 @@
|
|||
FE75A008237585DC001E78D9 /* NotificationEncryptionKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE75A007237585DC001E78D9 /* NotificationEncryptionKeys.swift */; };
|
||||
FE75A0102375F338001E78D9 /* PushEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE75A00E2375F324001E78D9 /* PushEventHandler.swift */; };
|
||||
FE75A0122376E73C001E78D9 /* SiskinPushNotificationsModuleProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE75A0112376E73C001E78D9 /* SiskinPushNotificationsModuleProvider.swift */; };
|
||||
FE7D293423B919FF001A877D /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7D293323B919FF001A877D /* DownloadManager.swift */; };
|
||||
FE7D293623BB5E0A001A877D /* LinkPreviewChatTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7D293523BB5E0A001A877D /* LinkPreviewChatTableViewCell.swift */; };
|
||||
FE7F645B1D281B1C00B9DF56 /* DBCapabilitiesCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7F645A1D281B1C00B9DF56 /* DBCapabilitiesCache.swift */; };
|
||||
FE7F9303200FD5AC004C6195 /* AccountManagerScramSaltedPasswordCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7F9302200FD5AC004C6195 /* AccountManagerScramSaltedPasswordCache.swift */; };
|
||||
FE80BDA81D928AD2001914B0 /* TigaseSwift.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FE80BDA71D928AD2001914B0 /* TigaseSwift.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -123,6 +126,7 @@
|
|||
FE9E136F1F26049A005C0EE5 /* NotificationSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE9E136E1F26049A005C0EE5 /* NotificationSettingsViewController.swift */; };
|
||||
FE9E13711F2606E9005C0EE5 /* ContactsSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE9E13701F2606E9005C0EE5 /* ContactsSettingsViewController.swift */; };
|
||||
FE9E13731F260B33005C0EE5 /* StepperTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE9E13721F260B33005C0EE5 /* StepperTableViewCell.swift */; };
|
||||
FE9EA16B23BF9DB2008C401A /* ChatAttachementsCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE9EA16A23BF9DB2008C401A /* ChatAttachementsCellView.swift */; };
|
||||
FEA308D01F27A063002EF4C0 /* NavigationControllerWrappingSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEA308CF1F27A063002EF4C0 /* NavigationControllerWrappingSegue.swift */; };
|
||||
FEA370BE1F3F3F8B0050CBAC /* TigaseSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE80BDA71D928AD2001914B0 /* TigaseSwift.framework */; };
|
||||
FEA7BF5B21E50C5800D9E36C /* JingleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEA7BF5A21E50C5800D9E36C /* JingleManager.swift */; };
|
||||
|
@ -138,6 +142,15 @@
|
|||
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 */; };
|
||||
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 */; };
|
||||
FECEF29223B792C6007EC323 /* ChatLinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECEF29123B792C6007EC323 /* ChatLinkPreview.swift */; };
|
||||
FECEF29423B7933A007EC323 /* MetadataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECEF29323B7933A007EC323 /* MetadataCache.swift */; };
|
||||
FECEF29623B7B076007EC323 /* DownloadStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECEF29523B7B076007EC323 /* DownloadStore.swift */; };
|
||||
FECEF29823B7B838007EC323 /* db-schema-9.sql in Resources */ = {isa = PBXBuildFile; fileRef = FECEF29723B7B838007EC323 /* db-schema-9.sql */; };
|
||||
FECEF29B23B7BC02007EC323 /* BaseChatTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECEF29923B7BBD3007EC323 /* BaseChatTableViewCell.swift */; };
|
||||
FECEF29E23B7C390007EC323 /* AttachmentChatTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FECEF29C23B7C36F007EC323 /* AttachmentChatTableViewCell.swift */; };
|
||||
FED353852270BFE600B69C53 /* openssl.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FED353822270BFD300B69C53 /* openssl.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
FED353862270BFE600B69C53 /* TigaseSwiftOMEMO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FED353732270BBA500B69C53 /* TigaseSwiftOMEMO.framework */; };
|
||||
FED353872270BFE600B69C53 /* TigaseSwiftOMEMO.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FED353732270BBA500B69C53 /* TigaseSwiftOMEMO.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -298,6 +311,7 @@
|
|||
FE4071E321E2605900F09B58 /* VideoCallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCallController.swift; sourceTree = "<group>"; };
|
||||
FE4071E521E262D900F09B58 /* VoIP.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = VoIP.storyboard; sourceTree = "<group>"; };
|
||||
FE4071E721E2653700F09B58 /* RoundButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundButton.swift; sourceTree = "<group>"; };
|
||||
FE43E43723BF3DE80079BD9B /* ChatAttachementsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAttachementsController.swift; sourceTree = "<group>"; };
|
||||
FE43EB541F3CC55900A4CAAD /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
|
||||
FE43EB561F3DBAAE00A4CAAD /* BaseChatViewController_PreviewExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseChatViewController_PreviewExtension.swift; sourceTree = "<group>"; };
|
||||
FE4496C31F87911C009F649C /* db-schema-3.sql */ = {isa = PBXFileReference; lastKnownFileType = text; path = "db-schema-3.sql"; sourceTree = "<group>"; };
|
||||
|
@ -361,6 +375,8 @@
|
|||
FE75A007237585DC001E78D9 /* NotificationEncryptionKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationEncryptionKeys.swift; sourceTree = "<group>"; };
|
||||
FE75A00E2375F324001E78D9 /* PushEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushEventHandler.swift; sourceTree = "<group>"; };
|
||||
FE75A0112376E73C001E78D9 /* SiskinPushNotificationsModuleProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiskinPushNotificationsModuleProvider.swift; sourceTree = "<group>"; };
|
||||
FE7D293323B919FF001A877D /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = "<group>"; };
|
||||
FE7D293523BB5E0A001A877D /* LinkPreviewChatTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewChatTableViewCell.swift; sourceTree = "<group>"; };
|
||||
FE7F645A1D281B1C00B9DF56 /* DBCapabilitiesCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DBCapabilitiesCache.swift; sourceTree = "<group>"; };
|
||||
FE7F9302200FD5AC004C6195 /* AccountManagerScramSaltedPasswordCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountManagerScramSaltedPasswordCache.swift; sourceTree = "<group>"; };
|
||||
FE80BDA71D928AD2001914B0 /* TigaseSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TigaseSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -384,6 +400,7 @@
|
|||
FE9E136E1F26049A005C0EE5 /* NotificationSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSettingsViewController.swift; sourceTree = "<group>"; };
|
||||
FE9E13701F2606E9005C0EE5 /* ContactsSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsSettingsViewController.swift; sourceTree = "<group>"; };
|
||||
FE9E13721F260B33005C0EE5 /* StepperTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepperTableViewCell.swift; sourceTree = "<group>"; };
|
||||
FE9EA16A23BF9DB2008C401A /* ChatAttachementsCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAttachementsCellView.swift; sourceTree = "<group>"; };
|
||||
FEA308CF1F27A063002EF4C0 /* NavigationControllerWrappingSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationControllerWrappingSegue.swift; sourceTree = "<group>"; };
|
||||
FEA7BF5A21E50C5800D9E36C /* JingleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JingleManager.swift; sourceTree = "<group>"; };
|
||||
FEA7BF5C21E50CAB00D9E36C /* JingleManager_Session.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JingleManager_Session.swift; sourceTree = "<group>"; };
|
||||
|
@ -398,6 +415,15 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
FECEF29123B792C6007EC323 /* ChatLinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatLinkPreview.swift; sourceTree = "<group>"; };
|
||||
FECEF29323B7933A007EC323 /* MetadataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataCache.swift; sourceTree = "<group>"; };
|
||||
FECEF29523B7B076007EC323 /* DownloadStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadStore.swift; sourceTree = "<group>"; };
|
||||
FECEF29723B7B838007EC323 /* db-schema-9.sql */ = {isa = PBXFileReference; lastKnownFileType = text; path = "db-schema-9.sql"; sourceTree = "<group>"; };
|
||||
FECEF29923B7BBD3007EC323 /* BaseChatTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseChatTableViewCell.swift; sourceTree = "<group>"; };
|
||||
FECEF29C23B7C36F007EC323 /* AttachmentChatTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentChatTableViewCell.swift; sourceTree = "<group>"; };
|
||||
FED353732270BBA500B69C53 /* TigaseSwiftOMEMO.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TigaseSwiftOMEMO.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
FED353822270BFD300B69C53 /* openssl.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = openssl.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
FED353882270C1D000B69C53 /* DBOMEMOStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBOMEMOStore.swift; sourceTree = "<group>"; };
|
||||
|
@ -537,6 +563,15 @@
|
|||
FEF19F0D23479F4C005CFE9A /* ChatViewDataSource.swift */,
|
||||
FEF19F152348E781005CFE9A /* BaseChatViewControllerWithDataSource.swift */,
|
||||
FE74D50F234A4E1F001A925B /* ChatTableViewSystemCell.swift */,
|
||||
FECEF28B23B79151007EC323 /* ChatEntry.swift */,
|
||||
FECEF28D23B7919E007EC323 /* ChatMessage.swift */,
|
||||
FECEF28F23B7926E007EC323 /* ChatAttachment.swift */,
|
||||
FECEF29123B792C6007EC323 /* ChatLinkPreview.swift */,
|
||||
FECEF29923B7BBD3007EC323 /* BaseChatTableViewCell.swift */,
|
||||
FECEF29C23B7C36F007EC323 /* AttachmentChatTableViewCell.swift */,
|
||||
FE7D293523BB5E0A001A877D /* LinkPreviewChatTableViewCell.swift */,
|
||||
FE43E43723BF3DE80079BD9B /* ChatAttachementsController.swift */,
|
||||
FE9EA16A23BF9DB2008C401A /* ChatAttachementsCellView.swift */,
|
||||
);
|
||||
path = chats;
|
||||
sourceTree = "<group>";
|
||||
|
@ -635,6 +670,9 @@
|
|||
FE75A004237475CD001E78D9 /* MainNotificationManagerProvider.swift */,
|
||||
FE75A0112376E73C001E78D9 /* SiskinPushNotificationsModuleProvider.swift */,
|
||||
FEDC678D238B03C1005C0FAB /* AppStoryboard.swift */,
|
||||
FECEF29323B7933A007EC323 /* MetadataCache.swift */,
|
||||
FECEF29523B7B076007EC323 /* DownloadStore.swift */,
|
||||
FE7D293323B919FF001A877D /* DownloadManager.swift */,
|
||||
);
|
||||
path = util;
|
||||
sourceTree = "<group>";
|
||||
|
@ -678,6 +716,7 @@
|
|||
FE759FC72370B2A4001E78D9 /* Shared.h */,
|
||||
FE759FC82370B2A4001E78D9 /* Info.plist */,
|
||||
FE759FF823742AC1001E78D9 /* NotificationCategory.swift */,
|
||||
FECEF29723B7B838007EC323 /* db-schema-9.sql */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1008,6 +1047,7 @@
|
|||
FE759FF22371F21C001E78D9 /* db-schema-6.sql in Resources */,
|
||||
FE759FEF2371F21C001E78D9 /* db-schema-3.sql in Resources */,
|
||||
FE759FED2371F213001E78D9 /* db-schema-2.sql in Resources */,
|
||||
FECEF29823B7B838007EC323 /* db-schema-9.sql in Resources */,
|
||||
FE759FF02371F21C001E78D9 /* db-schema-4.sql in Resources */,
|
||||
FE759FF32371F21C001E78D9 /* db-schema-7.sql in Resources */,
|
||||
FE759FF12371F21C001E78D9 /* db-schema-5.sql in Resources */,
|
||||
|
@ -1123,7 +1163,9 @@
|
|||
FE507A191CDB7B3B001A015C /* DBChatHistoryStore.swift in Sources */,
|
||||
FEA8D65D1F2F6AF60077C12F /* VCardEntryTypeAwareTableViewCell.swift in Sources */,
|
||||
FEDC6790238B05E4005C0FAB /* BlockedContactsController.swift in Sources */,
|
||||
FECEF29423B7933A007EC323 /* MetadataCache.swift in Sources */,
|
||||
FEB62C501DA80956001500D5 /* AvatarStore.swift in Sources */,
|
||||
FE7D293423B919FF001A877D /* DownloadManager.swift in Sources */,
|
||||
FE137A4821F6464D006B7F7C /* UIColor_mix.swift in Sources */,
|
||||
FE507A1E1CDB7B3B001A015C /* RosterViewController.swift in Sources */,
|
||||
FEDCBF691D9C53BA00AE9129 /* RosterProviderGrouped.swift in Sources */,
|
||||
|
@ -1137,7 +1179,11 @@
|
|||
FE507A1F1CDB7B3B001A015C /* AccountTableViewCell.swift in Sources */,
|
||||
FEF19F122348A3B8005CFE9A /* AvatarEventHandler.swift in Sources */,
|
||||
FE7F645B1D281B1C00B9DF56 /* DBCapabilitiesCache.swift in Sources */,
|
||||
FECEF29023B7926E007EC323 /* ChatAttachment.swift in Sources */,
|
||||
FE43E43823BF3DE80079BD9B /* ChatAttachementsController.swift in Sources */,
|
||||
FEDE93891D081C3D00CA60A9 /* Settings.swift in Sources */,
|
||||
FECEF29B23B7BC02007EC323 /* BaseChatTableViewCell.swift in Sources */,
|
||||
FECEF29E23B7C390007EC323 /* AttachmentChatTableViewCell.swift in Sources */,
|
||||
FED353892270C1D000B69C53 /* DBOMEMOStore.swift in Sources */,
|
||||
FE8DD9C5221B153A0090F5AA /* InviteViewController.swift in Sources */,
|
||||
FEB5EC9D1F6AE448007FE0E7 /* BaseChatViewControllerWithDataSourceContextMenuAndToolbar.swift in Sources */,
|
||||
|
@ -1150,8 +1196,10 @@
|
|||
FE759FFC23742CE5001E78D9 /* NotificationCenterDelegate.swift in Sources */,
|
||||
FEF19F0C23476466005CFE9A /* MucEventHandler.swift in Sources */,
|
||||
FE1DCCA21EA52CE200850563 /* DataFormController.swift in Sources */,
|
||||
FE7D293623BB5E0A001A877D /* LinkPreviewChatTableViewCell.swift in Sources */,
|
||||
FE6545641E9E8B67006A14AC /* ServerSelectorTableViewCell.swift in Sources */,
|
||||
FE31291A22240BEB00A92863 /* PEPBookmarksModule_extension.swift in Sources */,
|
||||
FECEF28E23B7919E007EC323 /* ChatMessage.swift in Sources */,
|
||||
FE00157F2019090300490340 /* ExperimentalSettingsViewController.swift in Sources */,
|
||||
FE75A006237475E2001E78D9 /* MainNotificationManagerProvider.swift in Sources */,
|
||||
FEDE93871D07564F00CA60A9 /* SwitchTableViewCell.swift in Sources */,
|
||||
|
@ -1172,6 +1220,7 @@
|
|||
FE507A151CDB7B3B001A015C /* ChatsListTableViewCell.swift in Sources */,
|
||||
FE43EB551F3CC55900A4CAAD /* ImageCache.swift in Sources */,
|
||||
FE507A1C1CDB7B3B001A015C /* DBRosterStore.swift in Sources */,
|
||||
FE9EA16B23BF9DB2008C401A /* ChatAttachementsCellView.swift in Sources */,
|
||||
FEF19F102348A046005CFE9A /* PresenceRosterEventHandler.swift in Sources */,
|
||||
FE94E5251CCBA74F00FAE755 /* AppDelegate.swift in Sources */,
|
||||
FE137A4A21F72AEA006B7F7C /* Appearance.swift in Sources */,
|
||||
|
@ -1199,7 +1248,10 @@
|
|||
FE75A0102375F338001E78D9 /* PushEventHandler.swift in Sources */,
|
||||
FEDCBF671D9C3EE700AE9129 /* RosterProviderFlat.swift in Sources */,
|
||||
FE9E136D1F25F5F7005C0EE5 /* ChatSettingsViewController.swift in Sources */,
|
||||
FECEF29623B7B076007EC323 /* DownloadStore.swift in Sources */,
|
||||
FEF19F08234751FF005CFE9A /* ChatViewItemProtocol.swift in Sources */,
|
||||
FECEF28C23B79151007EC323 /* ChatEntry.swift in Sources */,
|
||||
FECEF29223B792C6007EC323 /* ChatLinkPreview.swift in Sources */,
|
||||
FE9E13711F2606E9005C0EE5 /* ContactsSettingsViewController.swift in Sources */,
|
||||
FE00157D2017617B00490340 /* StreamFeaturesCache.swift in Sources */,
|
||||
FEDE938E1D09BB6200CA60A9 /* VCardEditBasicTableViewCell.swift in Sources */,
|
||||
|
|
|
@ -49,6 +49,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
}
|
||||
}
|
||||
try! DBConnection.migrateToGroupIfNeeded();
|
||||
ImageCache.convertToAttachments();
|
||||
RTCInitFieldTrialDictionary([:]);
|
||||
RTCInitializeSSL();
|
||||
RTCSetupInternalTracer();
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="vAU-gJ-Tx3">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="vAU-gJ-Tx3">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15510"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="collection view cell content view" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
|
@ -255,152 +256,16 @@
|
|||
<rect key="frame" x="0.0" y="0.0" width="600" height="579"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" dataMode="prototypes" style="plain" separatorStyle="default" allowsSelection="NO" allowsSelectionDuringEditing="YES" allowsMultipleSelectionDuringEditing="YES" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="Mpx-EA-82o">
|
||||
<tableView contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" dataMode="prototypes" style="plain" separatorStyle="default" allowsSelection="NO" allowsSelectionDuringEditing="YES" allowsMultipleSelectionDuringEditing="YES" rowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="Mpx-EA-82o">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="579"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<gestureRecognizers/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="ChatTableViewCellOutgoing" selectionStyle="default" indentationWidth="10" reuseIdentifier="ChatTableViewCellOutgoing" id="R0Z-Gr-PEt" customClass="ChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="ChatTableViewCell" selectionStyle="default" indentationWidth="10" reuseIdentifier="ChatTableViewMessageCell" id="LeS-8c-zb0" customClass="ChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="28" width="600" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="R0Z-Gr-PEt" id="bgf-5y-X6E">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" horizontalHuggingPriority="252" verticalHuggingPriority="252" horizontalCompressionResistancePriority="752" verticalCompressionResistancePriority="752" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="hny-rs-7Zh">
|
||||
<rect key="frame" x="30" y="22" width="534" height="126"/>
|
||||
<subviews>
|
||||
<label opaque="NO" contentMode="right" horizontalHuggingPriority="253" verticalHuggingPriority="253" horizontalCompressionResistancePriority="755" verticalCompressionResistancePriority="755" text="Label" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="f43-Vp-tGL">
|
||||
<rect key="frame" x="4" y="4" width="35.5" height="17"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" staticText="YES" allowsDirectInteraction="YES"/>
|
||||
</accessibility>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" contentMode="scaleAspectFill" horizontalHuggingPriority="249" verticalHuggingPriority="249" verticalCompressionResistancePriority="754" translatesAutoresizingMaskIntoConstraints="NO" id="3Ea-xK-ixM">
|
||||
<rect key="frame" x="0.0" y="25" width="43.5" height="0.0"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" image="YES" allowsDirectInteraction="YES"/>
|
||||
<bool key="isElement" value="YES"/>
|
||||
</accessibility>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" secondItem="3Ea-xK-ixM" secondAttribute="height" multiplier="3:2" id="hlf-BV-Owd"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="f43-Vp-tGL" firstAttribute="top" secondItem="hny-rs-7Zh" secondAttribute="top" constant="4" id="6n4-EN-Ael"/>
|
||||
<constraint firstItem="f43-Vp-tGL" firstAttribute="leading" secondItem="hny-rs-7Zh" secondAttribute="leading" constant="4" id="Boi-Em-A8s"/>
|
||||
<constraint firstAttribute="trailing" secondItem="f43-Vp-tGL" secondAttribute="trailing" constant="4" id="I3D-c0-KCd"/>
|
||||
<constraint firstItem="3Ea-xK-ixM" firstAttribute="top" secondItem="f43-Vp-tGL" secondAttribute="bottom" constant="4" id="TtK-j8-Y3y"/>
|
||||
<constraint firstAttribute="bottom" secondItem="3Ea-xK-ixM" secondAttribute="bottom" id="V95-PU-nYd"/>
|
||||
<constraint firstAttribute="trailing" secondItem="3Ea-xK-ixM" secondAttribute="trailing" id="aTh-zW-uoH"/>
|
||||
<constraint firstItem="3Ea-xK-ixM" firstAttribute="leading" secondItem="hny-rs-7Zh" secondAttribute="leading" id="ofS-wl-fYg"/>
|
||||
<constraint firstItem="3Ea-xK-ixM" firstAttribute="width" secondItem="hny-rs-7Zh" secondAttribute="width" id="u3p-bz-AP5"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Temporary label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eGw-mb-T3I">
|
||||
<rect key="frame" x="484" y="29" width="91" height="13"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<color key="textColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="eGw-mb-T3I" firstAttribute="top" secondItem="hny-rs-7Zh" secondAttribute="bottom" constant="2" id="1Ay-PZ-nuj"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="hny-rs-7Zh" secondAttribute="trailing" constant="2" id="2ch-AU-Mes"/>
|
||||
<constraint firstItem="hny-rs-7Zh" firstAttribute="top" secondItem="bgf-5y-X6E" secondAttribute="top" constant="2" id="Cns-N6-ktg"/>
|
||||
<constraint firstAttribute="bottom" secondItem="eGw-mb-T3I" secondAttribute="bottom" constant="2" id="fEf-hj-6qP"/>
|
||||
<constraint firstItem="hny-rs-7Zh" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="bgf-5y-X6E" secondAttribute="leadingMargin" constant="45" id="rk5-8k-eSN"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="eGw-mb-T3I" secondAttribute="trailing" constant="5" id="xXB-8t-ADq" userLabel="ItrailingMargin = Timestamp View.trailing + 7"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="messageFrameView" destination="hny-rs-7Zh" id="a6I-ZA-Ebp"/>
|
||||
<outlet property="messageTextView" destination="f43-Vp-tGL" id="lNK-Au-asZ"/>
|
||||
<outlet property="previewView" destination="3Ea-xK-ixM" id="hRE-ra-hYs"/>
|
||||
<outlet property="timestampView" destination="eGw-mb-T3I" id="s4e-RY-oTj"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="ChatTableViewCellIncoming" selectionStyle="default" indentationWidth="10" reuseIdentifier="ChatTableViewCellIncoming" id="2Y1-jf-e6K" customClass="ChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="72" width="600" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="2Y1-jf-e6K" id="ZfW-bW-Ai9">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Temporary label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="RG5-TE-xcd">
|
||||
<rect key="frame" x="41" y="27" width="91" height="14.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<color key="textColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="rEA-Xx-pue" customClass="AvatarView" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="2" y="2" width="30" height="30"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="30" id="42h-cw-1fr"/>
|
||||
<constraint firstAttribute="width" constant="30" id="efb-t0-Vvi"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<view contentMode="scaleToFill" ambiguous="YES" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="yI2-cn-AK5">
|
||||
<rect key="frame" x="53" y="2" width="532" height="42"/>
|
||||
<subviews>
|
||||
<label opaque="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" misplaced="YES" text="Incoming message" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vZk-ws-d3W">
|
||||
<rect key="frame" x="8" y="8" width="516" height="14"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" contentMode="scaleAspectFill" horizontalHuggingPriority="249" verticalHuggingPriority="249" verticalCompressionResistancePriority="754" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="WF6-5A-KDy">
|
||||
<rect key="frame" x="0.0" y="25" width="129" height="2"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" image="YES" allowsDirectInteraction="YES"/>
|
||||
<bool key="isElement" value="YES"/>
|
||||
</accessibility>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" secondItem="WF6-5A-KDy" secondAttribute="height" multiplier="3:2" id="aBB-Vh-NnE"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.93725490199999995" green="0.93725490199999995" blue="0.95686274510000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="WF6-5A-KDy" firstAttribute="top" secondItem="vZk-ws-d3W" secondAttribute="bottom" constant="4" id="2xt-xN-U4q"/>
|
||||
<constraint firstItem="vZk-ws-d3W" firstAttribute="leading" secondItem="yI2-cn-AK5" secondAttribute="leading" constant="4" id="AZY-J1-PVO"/>
|
||||
<constraint firstAttribute="trailing" secondItem="WF6-5A-KDy" secondAttribute="trailing" id="BRj-r0-q5J"/>
|
||||
<constraint firstItem="WF6-5A-KDy" firstAttribute="leading" secondItem="yI2-cn-AK5" secondAttribute="leading" id="P0d-a9-80D"/>
|
||||
<constraint firstItem="WF6-5A-KDy" firstAttribute="width" secondItem="yI2-cn-AK5" secondAttribute="width" id="Xcl-VO-09L"/>
|
||||
<constraint firstAttribute="bottom" secondItem="WF6-5A-KDy" secondAttribute="bottom" id="a8Z-L5-58h"/>
|
||||
<constraint firstItem="vZk-ws-d3W" firstAttribute="top" secondItem="yI2-cn-AK5" secondAttribute="top" constant="4" id="on7-zT-6cX"/>
|
||||
<constraint firstAttribute="trailing" secondItem="vZk-ws-d3W" secondAttribute="trailing" constant="4" id="rsw-5c-q1z"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="yI2-cn-AK5" firstAttribute="top" secondItem="ZfW-bW-Ai9" secondAttribute="top" constant="2" id="2GX-ZI-SJA"/>
|
||||
<constraint firstItem="yI2-cn-AK5" firstAttribute="leading" secondItem="rEA-Xx-pue" secondAttribute="trailing" constant="4" id="C6x-N7-tdS"/>
|
||||
<constraint firstItem="RG5-TE-xcd" firstAttribute="leading" secondItem="yI2-cn-AK5" secondAttribute="leading" constant="5" id="HWj-33-vQ9"/>
|
||||
<constraint firstItem="rEA-Xx-pue" firstAttribute="leading" secondItem="ZfW-bW-Ai9" secondAttribute="leading" constant="2" id="Kyo-Ot-saa"/>
|
||||
<constraint firstAttribute="trailingMargin" relation="greaterThanOrEqual" secondItem="yI2-cn-AK5" secondAttribute="trailing" constant="30" id="Wd8-VS-XIp"/>
|
||||
<constraint firstAttribute="bottom" secondItem="RG5-TE-xcd" secondAttribute="bottom" constant="2" id="ggs-F0-ci2"/>
|
||||
<constraint firstItem="RG5-TE-xcd" firstAttribute="top" secondItem="yI2-cn-AK5" secondAttribute="bottom" constant="2" id="m8F-3t-Iup"/>
|
||||
<constraint firstItem="rEA-Xx-pue" firstAttribute="top" secondItem="ZfW-bW-Ai9" secondAttribute="top" constant="2" id="wvS-IT-zEi"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="avatarView" destination="rEA-Xx-pue" id="bdR-Td-KOn"/>
|
||||
<outlet property="messageFrameView" destination="yI2-cn-AK5" id="YC2-rj-90s"/>
|
||||
<outlet property="messageTextView" destination="vZk-ws-d3W" id="gyT-Xt-57c"/>
|
||||
<outlet property="previewView" destination="WF6-5A-KDy" id="Lkb-N1-sT1"/>
|
||||
<outlet property="timestampView" destination="RG5-TE-xcd" id="Gns-G7-AEM"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="ChatTableViewCell" selectionStyle="default" indentationWidth="10" reuseIdentifier="ChatTableViewCell" rowHeight="71" id="LeS-8c-zb0" customClass="ChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="116" width="600" height="71"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="LeS-8c-zb0" id="Rua-Sn-7If">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="71"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="iWd-IQ-uK4" customClass="AvatarView" customModule="Siskin" customModuleProvider="target">
|
||||
|
@ -410,36 +275,27 @@
|
|||
<constraint firstAttribute="width" constant="30" id="djU-fH-v1r"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Incoming message" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oX2-6Y-0AV">
|
||||
<rect key="frame" x="44" y="25" width="534" height="17"/>
|
||||
<label contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Incoming message" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oX2-6Y-0AV">
|
||||
<rect key="frame" x="44" y="23" width="534" height="17"/>
|
||||
<color key="backgroundColor" name="systemBackground"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" contentMode="scaleAspectFill" horizontalHuggingPriority="249" verticalHuggingPriority="249" verticalCompressionResistancePriority="754" translatesAutoresizingMaskIntoConstraints="NO" id="Mcv-2T-EnV">
|
||||
<rect key="frame" x="44" y="42" width="534" height="25"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" image="YES" allowsDirectInteraction="YES"/>
|
||||
<bool key="isElement" value="YES"/>
|
||||
</accessibility>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" secondItem="Mcv-2T-EnV" secondAttribute="height" multiplier="3:2" id="zeM-8O-iut"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SyE-Pm-65B">
|
||||
<rect key="frame" x="44" y="5" width="38" height="18"/>
|
||||
<rect key="frame" x="44" y="5" width="38" height="16"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="600" text="Temporary label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="M8D-59-yPf">
|
||||
<rect key="frame" x="504" y="6" width="90" height="17"/>
|
||||
<rect key="frame" x="504" y="6" width="90" height="15"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="12"/>
|
||||
<color key="textColor" red="0.66666666669999997" green="0.66666666669999997" blue="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="EYL-KQ-TJ8">
|
||||
<rect key="frame" x="580" y="55" width="14" height="12"/>
|
||||
<rect key="frame" x="580" y="28" width="14" height="12"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="14" id="Qvz-MN-Dp0"/>
|
||||
</constraints>
|
||||
|
@ -452,57 +308,44 @@
|
|||
<constraint firstItem="M8D-59-yPf" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="SyE-Pm-65B" secondAttribute="trailing" constant="10" id="193-IR-xui"/>
|
||||
<constraint firstAttribute="trailing" secondItem="EYL-KQ-TJ8" secondAttribute="trailing" constant="6" id="8F2-PF-sGU"/>
|
||||
<constraint firstItem="M8D-59-yPf" firstAttribute="bottom" secondItem="SyE-Pm-65B" secondAttribute="bottom" id="92y-3d-Kxk"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Mcv-2T-EnV" secondAttribute="bottom" constant="4" id="9jm-OK-0kv"/>
|
||||
<constraint firstItem="EYL-KQ-TJ8" firstAttribute="bottom" secondItem="Mcv-2T-EnV" secondAttribute="bottom" id="FSJ-UH-Tnc"/>
|
||||
<constraint firstAttribute="bottom" secondItem="oX2-6Y-0AV" secondAttribute="bottom" constant="4" id="A8Z-ca-ojt"/>
|
||||
<constraint firstItem="SyE-Pm-65B" firstAttribute="leading" secondItem="iWd-IQ-uK4" secondAttribute="trailing" constant="8" id="SV8-FL-gWF"/>
|
||||
<constraint firstItem="oX2-6Y-0AV" firstAttribute="bottom" secondItem="Mcv-2T-EnV" secondAttribute="top" id="U6t-4h-cq5"/>
|
||||
<constraint firstItem="SyE-Pm-65B" firstAttribute="top" secondItem="iWd-IQ-uK4" secondAttribute="top" id="VDL-mB-wzM"/>
|
||||
<constraint firstItem="oX2-6Y-0AV" firstAttribute="top" secondItem="SyE-Pm-65B" secondAttribute="bottom" constant="2" id="WtY-t2-H3k"/>
|
||||
<constraint firstItem="EYL-KQ-TJ8" firstAttribute="leading" secondItem="oX2-6Y-0AV" secondAttribute="trailing" constant="2" id="aH6-OP-6JW"/>
|
||||
<constraint firstItem="oX2-6Y-0AV" firstAttribute="leading" secondItem="SyE-Pm-65B" secondAttribute="leading" id="f3c-Fe-CVc"/>
|
||||
<constraint firstAttribute="trailing" secondItem="M8D-59-yPf" secondAttribute="trailing" constant="6" id="gNr-48-DvR"/>
|
||||
<constraint firstItem="EYL-KQ-TJ8" firstAttribute="bottom" secondItem="oX2-6Y-0AV" secondAttribute="bottom" id="iGB-8y-WXw"/>
|
||||
<constraint firstItem="M8D-59-yPf" firstAttribute="top" secondItem="Rua-Sn-7If" secondAttribute="top" constant="6" id="kMt-uX-PXe"/>
|
||||
<constraint firstItem="EYL-KQ-TJ8" firstAttribute="top" relation="greaterThanOrEqual" secondItem="SyE-Pm-65B" secondAttribute="bottom" constant="2" id="lCR-3N-UdJ"/>
|
||||
<constraint firstItem="Mcv-2T-EnV" firstAttribute="leading" secondItem="oX2-6Y-0AV" secondAttribute="leading" id="no8-eR-p94"/>
|
||||
<constraint firstItem="Mcv-2T-EnV" firstAttribute="trailing" secondItem="oX2-6Y-0AV" secondAttribute="trailing" priority="900" id="qah-eG-G1t"/>
|
||||
<constraint firstItem="iWd-IQ-uK4" firstAttribute="top" secondItem="Rua-Sn-7If" secondAttribute="top" constant="5" id="sWq-aJ-A7j"/>
|
||||
<constraint firstItem="EYL-KQ-TJ8" firstAttribute="leading" secondItem="Mcv-2T-EnV" secondAttribute="trailing" constant="2" id="vod-z6-fD2"/>
|
||||
<constraint firstItem="iWd-IQ-uK4" firstAttribute="leading" secondItem="Rua-Sn-7If" secondAttribute="leading" constant="6" id="xIP-xZ-hud"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="avatarView" destination="iWd-IQ-uK4" id="rVR-v8-3Ti"/>
|
||||
<outlet property="avatarView" destination="iWd-IQ-uK4" id="8Wt-Po-TGX"/>
|
||||
<outlet property="messageTextView" destination="oX2-6Y-0AV" id="bgq-iM-elR"/>
|
||||
<outlet property="nicknameView" destination="SyE-Pm-65B" id="MEc-zm-tZk"/>
|
||||
<outlet property="previewView" destination="Mcv-2T-EnV" id="QqJ-WN-mpw"/>
|
||||
<outlet property="stateView" destination="EYL-KQ-TJ8" id="Enz-dH-ul3"/>
|
||||
<outlet property="timestampView" destination="M8D-59-yPf" id="SRn-Jc-eUW"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="ChatTableViewCellContinuation" selectionStyle="default" indentationWidth="10" reuseIdentifier="ChatTableViewCellContinuation" rowHeight="71" id="P2O-3P-PLi" customClass="ChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="187" width="600" height="71"/>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="ChatTableViewCellContinuation" selectionStyle="default" indentationWidth="10" reuseIdentifier="ChatTableViewMessageContinuationCell" id="P2O-3P-PLi" customClass="ChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="72" width="600" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="P2O-3P-PLi" id="Vhg-9a-XMw">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="71"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Incoming message" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fL0-TM-g7L">
|
||||
<rect key="frame" x="44" y="2" width="534" height="17"/>
|
||||
<label contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Incoming message" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fL0-TM-g7L">
|
||||
<rect key="frame" x="44" y="2" width="534" height="38"/>
|
||||
<color key="backgroundColor" name="systemBackground"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" contentMode="scaleAspectFill" horizontalHuggingPriority="249" verticalHuggingPriority="249" verticalCompressionResistancePriority="754" translatesAutoresizingMaskIntoConstraints="NO" id="FuR-Oq-Y9P">
|
||||
<rect key="frame" x="44" y="19" width="534" height="48"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" image="YES" allowsDirectInteraction="YES"/>
|
||||
<bool key="isElement" value="YES"/>
|
||||
</accessibility>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" secondItem="FuR-Oq-Y9P" secondAttribute="height" multiplier="3:2" id="Ql7-Rx-mID"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="e9t-6H-zJS">
|
||||
<rect key="frame" x="580" y="55" width="14" height="12"/>
|
||||
<rect key="frame" x="580" y="28" width="14" height="12"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="14" id="b9J-S0-HiN"/>
|
||||
</constraints>
|
||||
|
@ -513,25 +356,126 @@
|
|||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="e9t-6H-zJS" firstAttribute="top" relation="greaterThanOrEqual" secondItem="Vhg-9a-XMw" secondAttribute="top" constant="2" id="12w-w8-q7H"/>
|
||||
<constraint firstItem="e9t-6H-zJS" firstAttribute="leading" secondItem="FuR-Oq-Y9P" secondAttribute="trailing" constant="2" id="Fm4-gy-brd"/>
|
||||
<constraint firstItem="e9t-6H-zJS" firstAttribute="leading" secondItem="fL0-TM-g7L" secondAttribute="trailing" constant="2" id="5nC-ck-rlm"/>
|
||||
<constraint firstAttribute="trailing" secondItem="e9t-6H-zJS" secondAttribute="trailing" constant="6" id="Hvt-wO-cXa"/>
|
||||
<constraint firstItem="FuR-Oq-Y9P" firstAttribute="trailing" secondItem="fL0-TM-g7L" secondAttribute="trailing" priority="900" id="IN9-gQ-XVf"/>
|
||||
<constraint firstAttribute="bottom" secondItem="FuR-Oq-Y9P" secondAttribute="bottom" constant="4" id="N5D-rN-yIW"/>
|
||||
<constraint firstItem="fL0-TM-g7L" firstAttribute="top" secondItem="Vhg-9a-XMw" secondAttribute="top" constant="2" id="NUj-eJ-yHM"/>
|
||||
<constraint firstItem="fL0-TM-g7L" firstAttribute="leading" secondItem="Vhg-9a-XMw" secondAttribute="leading" constant="44" id="PBP-uy-IJw"/>
|
||||
<constraint firstItem="fL0-TM-g7L" firstAttribute="bottom" secondItem="FuR-Oq-Y9P" secondAttribute="top" id="VVJ-Ur-2YJ"/>
|
||||
<constraint firstItem="FuR-Oq-Y9P" firstAttribute="leading" secondItem="fL0-TM-g7L" secondAttribute="leading" id="q0g-uo-QhW"/>
|
||||
<constraint firstItem="e9t-6H-zJS" firstAttribute="bottom" secondItem="FuR-Oq-Y9P" secondAttribute="bottom" id="sgr-yR-730"/>
|
||||
<constraint firstAttribute="bottom" secondItem="fL0-TM-g7L" secondAttribute="bottom" constant="4" id="i0l-5t-waC"/>
|
||||
<constraint firstItem="e9t-6H-zJS" firstAttribute="bottom" secondItem="fL0-TM-g7L" secondAttribute="bottom" id="mwE-Pa-JeS"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="messageTextView" destination="fL0-TM-g7L" id="jSl-eI-CW6"/>
|
||||
<outlet property="previewView" destination="FuR-Oq-Y9P" id="gz5-gw-icB"/>
|
||||
<outlet property="stateView" destination="e9t-6H-zJS" id="1ud-1v-Thh"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="ChatTableViewAttachmentCell" rowHeight="71" id="UMT-xD-QCU" customClass="AttachmentChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="116" width="600" height="71"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="UMT-xD-QCU" id="Lts-6C-Qxj">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="71"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="iXm-m9-zoy" customClass="AvatarView" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="6" y="5" width="30" height="30"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="30" id="Z33-6s-dKs"/>
|
||||
<constraint firstAttribute="height" constant="30" id="ddh-22-4QN"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Nqc-Bg-dn1">
|
||||
<rect key="frame" x="44" y="5" width="38" height="18"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="600" text="Temporary label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9Oa-Mh-i12">
|
||||
<rect key="frame" x="504" y="6" width="90" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="12"/>
|
||||
<color key="textColor" red="0.66666666669999997" green="0.66666666669999997" blue="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Label" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="uez-Go-Gxg">
|
||||
<rect key="frame" x="580" y="55" width="14" height="12"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="14" id="UlC-be-8lb"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="10"/>
|
||||
<color key="textColor" name="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="b6A-lN-VQE">
|
||||
<rect key="frame" x="44" y="25" width="534" height="42"/>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="9Oa-Mh-i12" secondAttribute="trailing" constant="6" id="0G9-3X-YzF"/>
|
||||
<constraint firstAttribute="bottom" secondItem="b6A-lN-VQE" secondAttribute="bottom" constant="4" id="1vw-r8-gN5"/>
|
||||
<constraint firstItem="iXm-m9-zoy" firstAttribute="top" secondItem="Lts-6C-Qxj" secondAttribute="top" constant="5" id="8i4-r4-nRN"/>
|
||||
<constraint firstItem="b6A-lN-VQE" firstAttribute="leading" secondItem="Nqc-Bg-dn1" secondAttribute="leading" id="99D-aP-haF"/>
|
||||
<constraint firstItem="Nqc-Bg-dn1" firstAttribute="leading" secondItem="iXm-m9-zoy" secondAttribute="trailing" constant="8" id="JH5-ma-c0h"/>
|
||||
<constraint firstItem="Nqc-Bg-dn1" firstAttribute="top" secondItem="iXm-m9-zoy" secondAttribute="top" id="JLR-yN-gwj"/>
|
||||
<constraint firstItem="9Oa-Mh-i12" firstAttribute="top" secondItem="Lts-6C-Qxj" secondAttribute="top" constant="6" id="VeB-NP-1wz"/>
|
||||
<constraint firstItem="9Oa-Mh-i12" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Nqc-Bg-dn1" secondAttribute="trailing" constant="10" id="eRm-o2-ve1"/>
|
||||
<constraint firstItem="uez-Go-Gxg" firstAttribute="top" relation="greaterThanOrEqual" secondItem="Nqc-Bg-dn1" secondAttribute="bottom" constant="2" id="gRg-Ri-Uz1"/>
|
||||
<constraint firstItem="b6A-lN-VQE" firstAttribute="top" secondItem="Nqc-Bg-dn1" secondAttribute="bottom" constant="2" id="gka-Ij-l3k"/>
|
||||
<constraint firstItem="9Oa-Mh-i12" firstAttribute="bottom" secondItem="Nqc-Bg-dn1" secondAttribute="bottom" id="qDh-Tf-5he"/>
|
||||
<constraint firstItem="uez-Go-Gxg" firstAttribute="leading" secondItem="b6A-lN-VQE" secondAttribute="trailing" constant="2" id="vEh-Cg-pK1"/>
|
||||
<constraint firstAttribute="trailing" secondItem="uez-Go-Gxg" secondAttribute="trailing" constant="6" id="xlF-fd-cUd"/>
|
||||
<constraint firstItem="iXm-m9-zoy" firstAttribute="leading" secondItem="Lts-6C-Qxj" secondAttribute="leading" constant="6" id="zrm-aD-pua"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="avatarView" destination="iXm-m9-zoy" id="lu4-LG-M5Z"/>
|
||||
<outlet property="customView" destination="b6A-lN-VQE" id="P42-Kl-gqv"/>
|
||||
<outlet property="nicknameView" destination="Nqc-Bg-dn1" id="aPw-Ny-09A"/>
|
||||
<outlet property="stateView" destination="uez-Go-Gxg" id="3Id-bt-R9B"/>
|
||||
<outlet property="timestampView" destination="9Oa-Mh-i12" id="n74-I5-X3p"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="ChatTableViewAttachmentContinuationCell" rowHeight="71" id="Bmp-Dq-3Re" customClass="AttachmentChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="187" width="600" height="71"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="Bmp-Dq-3Re" id="GwA-3m-vJe">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="71"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Label" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="tvq-lu-sxS">
|
||||
<rect key="frame" x="580" y="55" width="14" height="12"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="14" id="2Fs-fp-iUc"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="10"/>
|
||||
<color key="textColor" name="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="kbZ-a4-GWn">
|
||||
<rect key="frame" x="44" y="2" width="534" height="65"/>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="tvq-lu-sxS" firstAttribute="leading" secondItem="kbZ-a4-GWn" secondAttribute="trailing" constant="2" id="FLe-5F-Qax"/>
|
||||
<constraint firstAttribute="bottom" secondItem="kbZ-a4-GWn" secondAttribute="bottom" constant="4" id="WNP-qK-f7g"/>
|
||||
<constraint firstAttribute="trailing" secondItem="tvq-lu-sxS" secondAttribute="trailing" constant="6" id="aOb-H3-BnX"/>
|
||||
<constraint firstItem="kbZ-a4-GWn" firstAttribute="top" secondItem="GwA-3m-vJe" secondAttribute="top" constant="2" id="mgF-rq-ZEh"/>
|
||||
<constraint firstItem="kbZ-a4-GWn" firstAttribute="leading" secondItem="GwA-3m-vJe" secondAttribute="leading" constant="44" id="qad-d1-KYG"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="customView" destination="kbZ-a4-GWn" id="DGa-1v-cNX"/>
|
||||
<outlet property="stateView" destination="tvq-lu-sxS" id="ZdY-CB-7vm"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="ChatTableViewLinkPreviewCell" rowHeight="71" id="3YR-H5-lf7" customClass="LinkPreviewChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="258" width="600" height="71"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="3YR-H5-lf7" id="Aju-iM-m0V">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="71"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="ChatTableViewSystemCell" selectionStyle="default" indentationWidth="10" reuseIdentifier="ChatTableViewSystemCell" rowHeight="33" id="fRp-jo-yLX" customClass="ChatTableViewSystemCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="258" width="600" height="33"/>
|
||||
<rect key="frame" x="0.0" y="329" width="600" height="33"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="fRp-jo-yLX" id="9NW-Te-8zl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="33"/>
|
||||
|
@ -2488,6 +2432,26 @@
|
|||
<outlet property="trustSwitch" destination="bsQ-Fj-zSv" id="9e2-f4-TMH"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="AttachmentsCell" textLabel="EpU-tc-DIx" rowHeight="44" style="IBUITableViewCellStyleDefault" id="09h-vH-qSu" customClass="CustomTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="472.5" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="09h-vH-qSu" id="Khc-ES-FDl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="383" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Attachments" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="EpU-tc-DIx">
|
||||
<rect key="frame" x="20" y="0.0" width="355" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="N9z-ms-iaT" kind="show" identifier="chatShowAttachments" id="tSJ-Sm-lhD"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="rSR-7g-CFO" id="lPG-hT-w4i"/>
|
||||
|
@ -2712,6 +2676,55 @@
|
|||
</objects>
|
||||
<point key="canvasLocation" x="1444" y="2516.4917541229388"/>
|
||||
</scene>
|
||||
<!--Attachments-->
|
||||
<scene sceneID="LjN-O9-imb">
|
||||
<objects>
|
||||
<collectionViewController storyboardIdentifier="ChatAttachmentsController" title="Attachments" id="N9z-ms-iaT" customClass="ChatAttachmentsController" customModule="Siskin" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="cHc-Jl-VKK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="2" minimumInteritemSpacing="2" id="mzE-f3-Idj">
|
||||
<size key="itemSize" width="100" height="100"/>
|
||||
<size key="headerReferenceSize" width="0.0" height="0.0"/>
|
||||
<size key="footerReferenceSize" width="0.0" height="0.0"/>
|
||||
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
</collectionViewFlowLayout>
|
||||
<cells>
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="AttachmentCellView" id="N8o-sn-l8d" customClass="ChatAttachmentsCellView" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="100" height="100"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="rlN-au-jgK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="100" height="100"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="250" image="defaultAvatar" translatesAutoresizingMaskIntoConstraints="NO" id="Rrz-MO-8C7">
|
||||
<rect key="frame" x="0.0" y="0.0" width="100" height="100"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="Rrz-MO-8C7" firstAttribute="leading" secondItem="rlN-au-jgK" secondAttribute="leading" id="U81-x9-EBG"/>
|
||||
<constraint firstItem="Rrz-MO-8C7" firstAttribute="top" secondItem="rlN-au-jgK" secondAttribute="top" id="hT3-1t-03m"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Rrz-MO-8C7" secondAttribute="bottom" id="ixY-C5-9Mh"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Rrz-MO-8C7" secondAttribute="trailing" id="x9A-iE-ugo"/>
|
||||
</constraints>
|
||||
</collectionViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="imageField" destination="Rrz-MO-8C7" id="pNx-EL-tib"/>
|
||||
</connections>
|
||||
</collectionViewCell>
|
||||
</cells>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="N9z-ms-iaT" id="NMe-lQ-1cq"/>
|
||||
<outlet property="delegate" destination="N9z-ms-iaT" id="Bmm-gs-F7f"/>
|
||||
</connections>
|
||||
</collectionView>
|
||||
<navigationItem key="navigationItem" title="Attachments" id="C1j-4i-HDP"/>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="fYb-Gb-aLl" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1404.3478260869567" y="3669.6428571428569"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="rMj-nV-pAP">
|
||||
<objects>
|
||||
|
@ -2734,12 +2747,16 @@
|
|||
<resources>
|
||||
<image name="appLogo" width="1024" height="1024"/>
|
||||
<image name="attach" width="36" height="36"/>
|
||||
<image name="defaultAvatar" width="192" height="192"/>
|
||||
<image name="first" width="30" height="30"/>
|
||||
<image name="messageArchiving" width="50" height="50"/>
|
||||
<image name="send" width="36" height="36"/>
|
||||
<namedColor name="secondaryLabelColor">
|
||||
<color red="0.45100000500679016" green="0.45100000500679016" blue="0.47099998593330383" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</namedColor>
|
||||
<namedColor name="systemBackground">
|
||||
<color red="0.98039215686274506" green="0.98039215686274506" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="UqQ-Sj-6Dq"/>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15510"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
|
@ -17,164 +17,20 @@
|
|||
<rect key="frame" x="0.0" y="0.0" width="375" height="623"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" dataMode="prototypes" style="plain" separatorStyle="default" allowsSelection="NO" allowsSelectionDuringEditing="YES" allowsMultipleSelectionDuringEditing="YES" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="5el-fR-NA6">
|
||||
<tableView contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" dataMode="prototypes" style="plain" separatorStyle="default" allowsSelection="NO" allowsSelectionDuringEditing="YES" allowsMultipleSelectionDuringEditing="YES" rowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="5el-fR-NA6">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="623"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<gestureRecognizers/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="ChatTableViewCellOutgoing" selectionStyle="default" indentationWidth="10" reuseIdentifier="MucChatTableViewCellOutgoing" rowHeight="42" id="hfn-DY-9WO" customClass="ChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="28" width="375" height="42"/>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="MucChatTableViewCell" selectionStyle="default" indentationWidth="10" reuseIdentifier="MucChatTableViewMessageCell" rowHeight="44" id="Bao-z7-vK8" customClass="ChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="hfn-DY-9WO" id="VSo-6t-dZh">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="42"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Kka-jO-XHM">
|
||||
<rect key="frame" x="314.5" y="2" width="43.5" height="21.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" contentMode="right" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lvk-dU-PmL">
|
||||
<rect key="frame" x="4" y="4" width="35.5" height="13.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" contentMode="scaleAspectFill" horizontalHuggingPriority="249" verticalHuggingPriority="249" verticalCompressionResistancePriority="754" translatesAutoresizingMaskIntoConstraints="NO" id="WYw-Co-HWm">
|
||||
<rect key="frame" x="0.0" y="21.5" width="43.5" height="0.0"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" image="YES" allowsDirectInteraction="YES"/>
|
||||
<bool key="isElement" value="YES"/>
|
||||
</accessibility>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" secondItem="WYw-Co-HWm" secondAttribute="height" multiplier="3:2" id="QqH-ev-cVz"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="WYw-Co-HWm" firstAttribute="leading" secondItem="Kka-jO-XHM" secondAttribute="leading" id="BeW-Yr-ppU"/>
|
||||
<constraint firstItem="lvk-dU-PmL" firstAttribute="leading" secondItem="Kka-jO-XHM" secondAttribute="leading" constant="4" id="Ch8-Vw-NaL"/>
|
||||
<constraint firstItem="WYw-Co-HWm" firstAttribute="width" secondItem="Kka-jO-XHM" secondAttribute="width" id="Dsh-CQ-Kh9"/>
|
||||
<constraint firstAttribute="bottom" secondItem="WYw-Co-HWm" secondAttribute="bottom" id="E2x-pg-UDp"/>
|
||||
<constraint firstItem="WYw-Co-HWm" firstAttribute="top" secondItem="lvk-dU-PmL" secondAttribute="bottom" constant="4" id="Yxb-9K-fql"/>
|
||||
<constraint firstItem="lvk-dU-PmL" firstAttribute="top" secondItem="Kka-jO-XHM" secondAttribute="top" constant="4" id="bX4-Uz-AlK"/>
|
||||
<constraint firstAttribute="trailing" secondItem="WYw-Co-HWm" secondAttribute="trailing" id="bhq-ap-GPG"/>
|
||||
<constraint firstAttribute="trailing" secondItem="lvk-dU-PmL" secondAttribute="trailing" constant="4" id="zKQ-fx-0K7"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Temporary label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eu7-WK-b7v">
|
||||
<rect key="frame" x="264" y="25.5" width="91" height="14.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<color key="textColor" red="0.66666666669999997" green="0.66666666669999997" blue="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="Kka-jO-XHM" secondAttribute="trailing" constant="2" id="CKe-bZ-UGG"/>
|
||||
<constraint firstItem="Kka-jO-XHM" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="VSo-6t-dZh" secondAttribute="leadingMargin" constant="45" id="dez-3X-Ryt"/>
|
||||
<constraint firstAttribute="bottom" secondItem="eu7-WK-b7v" secondAttribute="bottom" constant="2" id="fZU-Yv-gYw"/>
|
||||
<constraint firstItem="eu7-WK-b7v" firstAttribute="top" secondItem="Kka-jO-XHM" secondAttribute="bottom" constant="2" id="kAK-gm-GHH"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="eu7-WK-b7v" secondAttribute="trailing" constant="5" id="mgq-5R-86Y"/>
|
||||
<constraint firstItem="Kka-jO-XHM" firstAttribute="top" secondItem="VSo-6t-dZh" secondAttribute="top" constant="2" id="n6f-pr-AmD"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="messageFrameView" destination="Kka-jO-XHM" id="JhX-bW-pfe"/>
|
||||
<outlet property="messageTextView" destination="lvk-dU-PmL" id="tkV-6C-rQK"/>
|
||||
<outlet property="previewView" destination="WYw-Co-HWm" id="bm0-4m-q62"/>
|
||||
<outlet property="timestampView" destination="eu7-WK-b7v" id="Cqh-6d-oqn"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="MucChatTableViewCellIncoming" selectionStyle="default" indentationWidth="10" reuseIdentifier="MucChatTableViewCellIncoming" id="W4v-4P-YD6" customClass="ChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="70" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="W4v-4P-YD6" id="ZXT-32-hvW">
|
||||
<tableViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="Bao-z7-vK8" id="FaA-wG-hX8">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Temporary label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iCS-Bz-wPg">
|
||||
<rect key="frame" x="41" y="27.5" width="52" height="14.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<color key="textColor" red="0.66666666669999997" green="0.66666666669999997" blue="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Sb3-cw-NYv">
|
||||
<rect key="frame" x="36" y="17" width="129" height="8.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Incoming message" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ouq-Ks-VIp">
|
||||
<rect key="frame" x="4" y="4" width="121" height="0.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" contentMode="scaleAspectFill" horizontalHuggingPriority="249" verticalHuggingPriority="249" verticalCompressionResistancePriority="754" translatesAutoresizingMaskIntoConstraints="NO" id="I01-Rg-O9P">
|
||||
<rect key="frame" x="0.0" y="8.5" width="129" height="0.0"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" image="YES" allowsDirectInteraction="YES"/>
|
||||
<bool key="isElement" value="YES"/>
|
||||
</accessibility>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" secondItem="I01-Rg-O9P" secondAttribute="height" multiplier="3:2" id="fmP-0a-EwM"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.93725490199999995" green="0.93725490199999995" blue="0.95686274510000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="ouq-Ks-VIp" firstAttribute="leading" secondItem="Sb3-cw-NYv" secondAttribute="leading" constant="4" id="Bw6-Bo-mtB"/>
|
||||
<constraint firstAttribute="bottom" secondItem="I01-Rg-O9P" secondAttribute="bottom" id="GkE-T1-J89"/>
|
||||
<constraint firstItem="I01-Rg-O9P" firstAttribute="top" secondItem="ouq-Ks-VIp" secondAttribute="bottom" constant="4" id="V6p-dM-Asw"/>
|
||||
<constraint firstItem="ouq-Ks-VIp" firstAttribute="top" secondItem="Sb3-cw-NYv" secondAttribute="top" constant="4" id="bKi-5L-dO7"/>
|
||||
<constraint firstItem="I01-Rg-O9P" firstAttribute="leading" secondItem="Sb3-cw-NYv" secondAttribute="leading" id="jTb-Bo-ouH"/>
|
||||
<constraint firstAttribute="trailing" secondItem="ouq-Ks-VIp" secondAttribute="trailing" constant="4" id="jn4-Us-sKa"/>
|
||||
<constraint firstItem="I01-Rg-O9P" firstAttribute="width" secondItem="Sb3-cw-NYv" secondAttribute="width" id="ttE-GF-Mqj"/>
|
||||
<constraint firstAttribute="trailing" secondItem="I01-Rg-O9P" secondAttribute="trailing" id="zOp-Se-wVT"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" verticalCompressionResistancePriority="751" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" highlighted="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="CML-cD-nxX" userLabel="Nickname">
|
||||
<rect key="frame" x="20" y="2" width="28" height="13"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="11"/>
|
||||
<color key="textColor" red="0.33333333329999998" green="0.33333333329999998" blue="0.33333333329999998" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="nru-BN-5oN" customClass="AvatarView" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="2" y="17" width="30" height="30"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="30" id="Aue-Ex-hYK"/>
|
||||
<constraint firstAttribute="height" constant="30" id="Dhi-Kj-BFK"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="iCS-Bz-wPg" secondAttribute="trailing" constant="282" id="1WR-8M-PlW"/>
|
||||
<constraint firstItem="iCS-Bz-wPg" firstAttribute="top" secondItem="Sb3-cw-NYv" secondAttribute="bottom" constant="2" id="84E-vj-ozO"/>
|
||||
<constraint firstItem="CML-cD-nxX" firstAttribute="top" secondItem="ZXT-32-hvW" secondAttribute="top" constant="2" id="8P3-eq-Ozj"/>
|
||||
<constraint firstItem="nru-BN-5oN" firstAttribute="top" secondItem="CML-cD-nxX" secondAttribute="bottom" constant="2" id="BeG-Et-Vvs"/>
|
||||
<constraint firstAttribute="trailingMargin" relation="greaterThanOrEqual" secondItem="Sb3-cw-NYv" secondAttribute="trailing" constant="30" id="EFT-Wk-Km5"/>
|
||||
<constraint firstItem="nru-BN-5oN" firstAttribute="leading" secondItem="ZXT-32-hvW" secondAttribute="leading" constant="2" id="FpX-9e-3VG"/>
|
||||
<constraint firstItem="Sb3-cw-NYv" firstAttribute="top" secondItem="CML-cD-nxX" secondAttribute="bottom" constant="2" id="LnF-H2-0dG"/>
|
||||
<constraint firstItem="iCS-Bz-wPg" firstAttribute="leading" secondItem="ZXT-32-hvW" secondAttribute="leading" constant="41" id="X4t-6L-cLd"/>
|
||||
<constraint firstItem="Sb3-cw-NYv" firstAttribute="leading" secondItem="nru-BN-5oN" secondAttribute="trailing" constant="4" id="Xja-UD-G24"/>
|
||||
<constraint firstItem="CML-cD-nxX" firstAttribute="leading" secondItem="ZXT-32-hvW" secondAttribute="leadingMargin" constant="5" id="bPt-82-1CC"/>
|
||||
<constraint firstAttribute="bottom" secondItem="iCS-Bz-wPg" secondAttribute="bottom" constant="2" id="cWa-BH-kDw"/>
|
||||
<constraint firstItem="iCS-Bz-wPg" firstAttribute="leading" secondItem="Sb3-cw-NYv" secondAttribute="leading" constant="5" id="jIP-Lh-mpi"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="avatarView" destination="nru-BN-5oN" id="2dR-dA-mb4"/>
|
||||
<outlet property="messageFrameView" destination="Sb3-cw-NYv" id="k3R-lr-7rG"/>
|
||||
<outlet property="messageTextView" destination="ouq-Ks-VIp" id="omm-G3-p7d"/>
|
||||
<outlet property="nicknameView" destination="CML-cD-nxX" id="oMy-br-Kgm"/>
|
||||
<outlet property="previewView" destination="I01-Rg-O9P" id="eKi-Cg-7Xm"/>
|
||||
<outlet property="timestampView" destination="iCS-Bz-wPg" id="NfP-FF-Xyb"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="MucChatTableViewCell" selectionStyle="default" indentationWidth="10" reuseIdentifier="MucChatTableViewCell" rowHeight="106" id="Bao-z7-vK8" customClass="ChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="114" width="375" height="106"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="Bao-z7-vK8" id="FaA-wG-hX8">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="106"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Temporary label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="tX3-Qg-Trf">
|
||||
<rect key="frame" x="279.5" y="6" width="89.5" height="17"/>
|
||||
<rect key="frame" x="279.5" y="6" width="89.5" height="15"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="12"/>
|
||||
<color key="textColor" red="0.66666666669999997" green="0.66666666669999997" blue="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
|
@ -186,24 +42,15 @@
|
|||
<constraint firstAttribute="height" constant="30" id="KtH-CC-WE1"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Incoming message" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rfj-q1-YJd">
|
||||
<rect key="frame" x="44" y="25" width="325" height="17"/>
|
||||
<label contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Incoming message" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rfj-q1-YJd">
|
||||
<rect key="frame" x="44" y="23" width="325" height="17"/>
|
||||
<color key="backgroundColor" name="systemBackground"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" contentMode="scaleAspectFill" horizontalHuggingPriority="249" verticalHuggingPriority="249" verticalCompressionResistancePriority="754" translatesAutoresizingMaskIntoConstraints="NO" id="hhF-rV-ZOC">
|
||||
<rect key="frame" x="44" y="42" width="325" height="60"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" image="YES" allowsDirectInteraction="YES"/>
|
||||
<bool key="isElement" value="YES"/>
|
||||
</accessibility>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" secondItem="hhF-rV-ZOC" secondAttribute="height" multiplier="3:2" id="HU2-np-we2"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" highlighted="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YMW-VZ-mYs" userLabel="Nickname">
|
||||
<rect key="frame" x="44" y="5" width="38" height="18"/>
|
||||
<rect key="frame" x="44" y="5" width="38" height="16"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="15"/>
|
||||
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
|
@ -211,71 +58,54 @@
|
|||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="rfj-q1-YJd" firstAttribute="leading" secondItem="YMW-VZ-mYs" secondAttribute="leading" id="3vj-UW-OH0"/>
|
||||
<constraint firstItem="hhF-rV-ZOC" firstAttribute="trailing" secondItem="rfj-q1-YJd" secondAttribute="trailing" id="7rl-y3-hks"/>
|
||||
<constraint firstItem="YMW-VZ-mYs" firstAttribute="leading" secondItem="lsv-vn-uow" secondAttribute="trailing" constant="8" id="B8Y-9k-Bch"/>
|
||||
<constraint firstItem="hhF-rV-ZOC" firstAttribute="leading" secondItem="rfj-q1-YJd" secondAttribute="leading" id="Chy-7k-zxq"/>
|
||||
<constraint firstItem="tX3-Qg-Trf" firstAttribute="top" secondItem="FaA-wG-hX8" secondAttribute="top" constant="6" id="GgD-fE-wn4"/>
|
||||
<constraint firstItem="lsv-vn-uow" firstAttribute="leading" secondItem="FaA-wG-hX8" secondAttribute="leading" constant="6" id="Gj1-dl-ZmK"/>
|
||||
<constraint firstItem="rfj-q1-YJd" firstAttribute="trailing" secondItem="tX3-Qg-Trf" secondAttribute="trailing" id="Gwl-2a-LuM"/>
|
||||
<constraint firstAttribute="trailing" secondItem="tX3-Qg-Trf" secondAttribute="trailing" constant="6" id="LJb-6w-qRi"/>
|
||||
<constraint firstItem="hhF-rV-ZOC" firstAttribute="top" secondItem="rfj-q1-YJd" secondAttribute="bottom" id="bKd-jc-wG8"/>
|
||||
<constraint firstItem="tX3-Qg-Trf" firstAttribute="bottom" secondItem="YMW-VZ-mYs" secondAttribute="bottom" id="cWJ-4y-avo"/>
|
||||
<constraint firstItem="YMW-VZ-mYs" firstAttribute="top" secondItem="lsv-vn-uow" secondAttribute="top" id="fiK-5j-FMH"/>
|
||||
<constraint firstItem="rfj-q1-YJd" firstAttribute="top" secondItem="YMW-VZ-mYs" secondAttribute="bottom" constant="2" id="qRR-Mg-4sD"/>
|
||||
<constraint firstAttribute="bottom" secondItem="hhF-rV-ZOC" secondAttribute="bottom" constant="4" id="qtB-d2-YRb"/>
|
||||
<constraint firstItem="tX3-Qg-Trf" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="YMW-VZ-mYs" secondAttribute="trailing" constant="10" id="uzn-XZ-lAs"/>
|
||||
<constraint firstItem="lsv-vn-uow" firstAttribute="top" secondItem="FaA-wG-hX8" secondAttribute="top" constant="5" id="waw-TD-b4j"/>
|
||||
<constraint firstAttribute="bottom" secondItem="rfj-q1-YJd" secondAttribute="bottom" constant="4" id="yq4-ky-nVW"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="avatarView" destination="lsv-vn-uow" id="EMf-xg-3on"/>
|
||||
<outlet property="messageTextView" destination="rfj-q1-YJd" id="IJP-SR-LHq"/>
|
||||
<outlet property="nicknameView" destination="YMW-VZ-mYs" id="rZH-gq-k4Q"/>
|
||||
<outlet property="previewView" destination="hhF-rV-ZOC" id="aFZ-DB-CYf"/>
|
||||
<outlet property="timestampView" destination="tX3-Qg-Trf" id="dd8-gu-hig"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="MucChatTableViewCellContinuation" selectionStyle="default" indentationWidth="10" reuseIdentifier="MucChatTableViewCellContinuation" rowHeight="106" id="dYP-lW-wpx" customClass="ChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="220" width="375" height="106"/>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="MucChatTableViewCellContinuation" selectionStyle="default" indentationWidth="10" reuseIdentifier="MucChatTableViewMessageContinuationCell" rowHeight="44" id="dYP-lW-wpx" customClass="ChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="72" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="dYP-lW-wpx" id="fko-5f-K5r">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="106"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Incoming message" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dEP-he-X8w">
|
||||
<rect key="frame" x="44" y="2" width="325" height="17"/>
|
||||
<label contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Incoming message" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dEP-he-X8w">
|
||||
<rect key="frame" x="44" y="2" width="325" height="38"/>
|
||||
<color key="backgroundColor" name="systemBackground"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" contentMode="scaleAspectFill" horizontalHuggingPriority="249" verticalHuggingPriority="249" verticalCompressionResistancePriority="754" translatesAutoresizingMaskIntoConstraints="NO" id="RsO-ps-NkX">
|
||||
<rect key="frame" x="44" y="19" width="325" height="83"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" image="YES" allowsDirectInteraction="YES"/>
|
||||
<bool key="isElement" value="YES"/>
|
||||
</accessibility>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" secondItem="RsO-ps-NkX" secondAttribute="height" multiplier="3:2" id="byc-Ki-gdv"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="RsO-ps-NkX" secondAttribute="bottom" constant="4" id="ApI-ah-xHN"/>
|
||||
<constraint firstItem="RsO-ps-NkX" firstAttribute="leading" secondItem="dEP-he-X8w" secondAttribute="leading" id="GKS-t3-996"/>
|
||||
<constraint firstItem="dEP-he-X8w" firstAttribute="leading" secondItem="fko-5f-K5r" secondAttribute="leading" constant="44" id="I07-rN-ZmV"/>
|
||||
<constraint firstItem="dEP-he-X8w" firstAttribute="top" secondItem="fko-5f-K5r" secondAttribute="top" constant="2" id="LwM-oa-9I1"/>
|
||||
<constraint firstAttribute="trailing" secondItem="dEP-he-X8w" secondAttribute="trailing" constant="6" id="M7N-f1-L3t"/>
|
||||
<constraint firstItem="RsO-ps-NkX" firstAttribute="top" secondItem="dEP-he-X8w" secondAttribute="bottom" id="fd7-O9-xfn"/>
|
||||
<constraint firstItem="RsO-ps-NkX" firstAttribute="trailing" secondItem="dEP-he-X8w" secondAttribute="trailing" id="l3o-aw-ggf"/>
|
||||
<constraint firstAttribute="bottom" secondItem="dEP-he-X8w" secondAttribute="bottom" constant="4" id="aBo-in-eY5"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="messageTextView" destination="dEP-he-X8w" id="17V-GJ-gQW"/>
|
||||
<outlet property="previewView" destination="RsO-ps-NkX" id="Ne3-fm-hvn"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="MucChatTableViewSystemCell" selectionStyle="default" indentationWidth="10" reuseIdentifier="MucChatTableViewSystemCell" rowHeight="33" id="gr6-vB-GaQ" customClass="ChatTableViewSystemCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="326" width="375" height="33"/>
|
||||
<rect key="frame" x="0.0" y="116" width="375" height="33"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="gr6-vB-GaQ" id="OfY-hw-4nf">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="33"/>
|
||||
|
@ -299,6 +129,111 @@
|
|||
<outlet property="messageView" destination="Nuv-6c-iva" id="s97-uf-THh"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="MucChatTableViewAttachmentContinuationCell" rowHeight="71" id="ahp-Qx-MWp" customClass="AttachmentChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="149" width="375" height="71"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="ahp-Qx-MWp" id="ac1-gM-yBJ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="71"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Label" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="P3i-AC-xFL">
|
||||
<rect key="frame" x="355" y="55" width="14" height="12"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="14" id="Y14-G0-nu5"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="10"/>
|
||||
<color key="textColor" name="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kTe-6g-st2">
|
||||
<rect key="frame" x="44" y="2" width="309" height="65"/>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="P3i-AC-xFL" firstAttribute="leading" secondItem="kTe-6g-st2" secondAttribute="trailing" constant="2" id="Cr4-sc-TUi"/>
|
||||
<constraint firstItem="kTe-6g-st2" firstAttribute="leading" secondItem="ac1-gM-yBJ" secondAttribute="leading" constant="44" id="VKP-mp-BKj"/>
|
||||
<constraint firstAttribute="bottom" secondItem="kTe-6g-st2" secondAttribute="bottom" constant="4" id="lWa-cb-8fb"/>
|
||||
<constraint firstItem="kTe-6g-st2" firstAttribute="top" secondItem="ac1-gM-yBJ" secondAttribute="top" constant="2" id="xjq-ms-Yzm"/>
|
||||
<constraint firstAttribute="trailing" secondItem="P3i-AC-xFL" secondAttribute="trailing" constant="6" id="xmU-ks-Mes"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="customView" destination="kTe-6g-st2" id="qJW-9m-d9Q"/>
|
||||
<outlet property="stateView" destination="P3i-AC-xFL" id="7uz-GN-cdl"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="MucChatTableViewLinkPreviewCell" rowHeight="71" id="xgr-md-2Kg" customClass="LinkPreviewChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="220" width="375" height="71"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="xgr-md-2Kg" id="y2W-Ns-4xi">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="71"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="MucChatTableViewAttachmentCell" rowHeight="71" id="hUG-WT-xM9" customClass="AttachmentChatTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="291" width="375" height="71"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="hUG-WT-xM9" id="jHQ-bY-717">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="71"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="GPo-sU-r92" customClass="AvatarView" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="6" y="5" width="30" height="30"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="30" id="7tH-nB-acb"/>
|
||||
<constraint firstAttribute="height" constant="30" id="BHn-yR-3Q2"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="uvx-Hg-CEC">
|
||||
<rect key="frame" x="44" y="5" width="38" height="18"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="600" text="Temporary label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Pks-KM-kQr">
|
||||
<rect key="frame" x="279" y="6" width="90" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="12"/>
|
||||
<color key="textColor" red="0.66666666669999997" green="0.66666666669999997" blue="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Label" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oxt-Kq-u6J">
|
||||
<rect key="frame" x="355" y="55" width="14" height="12"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="14" id="kYu-n7-bfd"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="10"/>
|
||||
<color key="textColor" name="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="KOz-ZW-EtH">
|
||||
<rect key="frame" x="44" y="25" width="309" height="42"/>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="Pks-KM-kQr" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="uvx-Hg-CEC" secondAttribute="trailing" constant="10" id="4NB-YE-zBd"/>
|
||||
<constraint firstItem="oxt-Kq-u6J" firstAttribute="leading" secondItem="KOz-ZW-EtH" secondAttribute="trailing" constant="2" id="6KF-mg-jgP"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Pks-KM-kQr" secondAttribute="trailing" constant="6" id="9Nn-fo-XE3"/>
|
||||
<constraint firstItem="uvx-Hg-CEC" firstAttribute="leading" secondItem="GPo-sU-r92" secondAttribute="trailing" constant="8" id="CqX-6J-WJk"/>
|
||||
<constraint firstAttribute="trailing" secondItem="oxt-Kq-u6J" secondAttribute="trailing" constant="6" id="Dlc-7d-qh5"/>
|
||||
<constraint firstAttribute="bottom" secondItem="KOz-ZW-EtH" secondAttribute="bottom" constant="4" id="F4d-Bp-nov"/>
|
||||
<constraint firstItem="KOz-ZW-EtH" firstAttribute="top" secondItem="uvx-Hg-CEC" secondAttribute="bottom" constant="2" id="Jxa-dM-39g"/>
|
||||
<constraint firstItem="GPo-sU-r92" firstAttribute="leading" secondItem="jHQ-bY-717" secondAttribute="leading" constant="6" id="SDt-uy-3SS"/>
|
||||
<constraint firstItem="Pks-KM-kQr" firstAttribute="top" secondItem="jHQ-bY-717" secondAttribute="top" constant="6" id="VGN-f5-Pvm"/>
|
||||
<constraint firstItem="oxt-Kq-u6J" firstAttribute="top" relation="greaterThanOrEqual" secondItem="uvx-Hg-CEC" secondAttribute="bottom" constant="2" id="a86-y0-420"/>
|
||||
<constraint firstItem="GPo-sU-r92" firstAttribute="top" secondItem="jHQ-bY-717" secondAttribute="top" constant="5" id="eJr-oL-kBd"/>
|
||||
<constraint firstItem="Pks-KM-kQr" firstAttribute="bottom" secondItem="uvx-Hg-CEC" secondAttribute="bottom" id="ged-sv-Op8"/>
|
||||
<constraint firstItem="uvx-Hg-CEC" firstAttribute="top" secondItem="GPo-sU-r92" secondAttribute="top" id="kUu-mp-gzq"/>
|
||||
<constraint firstItem="KOz-ZW-EtH" firstAttribute="leading" secondItem="uvx-Hg-CEC" secondAttribute="leading" id="qLQ-to-YAY"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="avatarView" destination="GPo-sU-r92" id="kXQ-Pa-1rN"/>
|
||||
<outlet property="customView" destination="KOz-ZW-EtH" id="GoL-hp-Al8"/>
|
||||
<outlet property="nicknameView" destination="uvx-Hg-CEC" id="m3g-WQ-xJ7"/>
|
||||
<outlet property="stateView" destination="oxt-Kq-u6J" id="vTW-DK-wco"/>
|
||||
<outlet property="timestampView" destination="Pks-KM-kQr" id="7CX-qx-dTy"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outletCollection property="gestureRecognizers" destination="Z94-Hf-jAY" appends="YES" id="0jw-xb-dCn"/>
|
||||
|
@ -698,6 +633,30 @@
|
|||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle=" " id="2ZD-5S-4hK">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="wBw-1J-Xau" style="IBUITableViewCellStyleDefault" id="3IN-v0-ayC" customClass="CustomTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="429.5" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3IN-v0-ayC" id="iXe-AG-FnD">
|
||||
<rect key="frame" x="0.0" y="0.0" width="348" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Attachments" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="wBw-1J-Xau">
|
||||
<rect key="frame" x="16" y="0.0" width="324" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="WK7-rl-udc" kind="show" identifier="chatShowAttachments" id="zCm-lW-ix4"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="IGA-uE-mHb" id="iEv-vE-Trn"/>
|
||||
|
@ -716,6 +675,16 @@
|
|||
</objects>
|
||||
<point key="canvasLocation" x="3529" y="649"/>
|
||||
</scene>
|
||||
<!--ChatAttachmentsController-->
|
||||
<scene sceneID="xFY-s8-0Ui">
|
||||
<objects>
|
||||
<viewControllerPlaceholder storyboardName="Main" referencedIdentifier="ChatAttachmentsController" id="WK7-rl-udc" sceneMemberID="viewController">
|
||||
<navigationItem key="navigationItem" id="JXN-3A-Aud"/>
|
||||
</viewControllerPlaceholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="TmV-Yt-wrX" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="4301" y="778"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="hfa-O8-oHo">
|
||||
<objects>
|
||||
|
@ -1160,5 +1129,8 @@
|
|||
<namedColor name="secondaryLabelColor">
|
||||
<color red="0.45100000500679016" green="0.45100000500679016" blue="0.47099998593330383" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</namedColor>
|
||||
<namedColor name="systemBackground">
|
||||
<color red="0.98039215686274506" green="0.98039215686274506" blue="0.98039215686274506" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
|
@ -84,6 +84,8 @@
|
|||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Used to save attachements from conversations</string>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
|
@ -91,5 +93,7 @@
|
|||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14868" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="kwm-Ck-fLB">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15510"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
|
@ -290,15 +290,15 @@
|
|||
<segue destination="FGQ-GL-dYt" kind="showDetail" customClass="NavigationControllerWrappingSegue" customModule="Siskin" id="gsA-2D-Fg6"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="aboutSettingsViewCall" selectionStyle="default" indentationWidth="10" reuseIdentifier="AboutSettingsViewCell" id="00j-hl-4km" userLabel="AboutSettingsViewCall" customClass="CustomTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="aboutSettingsViewCall" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="AboutSettingsViewCell" id="00j-hl-4km" userLabel="AboutSettingsViewCall" customClass="CustomTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="407.5" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="00j-hl-4km" id="BKo-8a-1ti">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="383" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="About" textAlignment="natural" lineBreakMode="wordWrap" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3vM-ad-4ap">
|
||||
<rect key="frame" x="20" y="11" width="374" height="22"/>
|
||||
<rect key="frame" x="20" y="11" width="355" height="22"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="accountName"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
|
@ -313,6 +313,9 @@
|
|||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<accessibility key="accessibilityConfiguration" identifier="aboutSettingsViewCall"/>
|
||||
<connections>
|
||||
<segue destination="5B4-ae-wmU" kind="show" id="JiS-ax-O98"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
|
@ -574,15 +577,15 @@
|
|||
<outlet property="switchView" destination="BwA-0i-nsF" id="p3D-DW-hiS"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="simplifiedLinkToFileTableViewCell" selectionStyle="default" indentationWidth="10" reuseIdentifier="SimplifiedLinkToFileTableViewCell" id="CHa-Jc-woM" userLabel="SimplifiedLinkToFileTableViewCell" customClass="SwitchTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="simplifiedLinkToFileTableViewCell" selectionStyle="default" indentationWidth="10" reuseIdentifier="LinkPreviewsTableViewCell" id="CHa-Jc-woM" userLabel="SimplifiedLinkToFileTableViewCell" customClass="SwitchTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="363.5" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="CHa-Jc-woM" id="hJd-r7-bvW">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Simplified link to HTTP file" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ybc-mt-hVG">
|
||||
<rect key="frame" x="20" y="12" width="200.5" height="20.5"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Show link previews" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ybc-mt-hVG">
|
||||
<rect key="frame" x="20" y="11.5" width="145.5" height="21"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="accountName"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
|
@ -649,8 +652,8 @@
|
|||
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Max image preview size" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hYx-To-bTs">
|
||||
<rect key="frame" x="20" y="12" width="182" height="20.5"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Auto file download limit" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hYx-To-bTs">
|
||||
<rect key="frame" x="20" y="12" width="177.5" height="20.5"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="accountName"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
|
@ -676,14 +679,14 @@
|
|||
</tableViewCellContentView>
|
||||
<accessibility key="accessibilityConfiguration" identifier="maxImagePreviewSizeTableViewCell"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="clearImagePreviewTableViewCell" selectionStyle="default" indentationWidth="10" reuseIdentifier="ClearImagePreviewTableViewCell" id="BNU-sl-iSH" userLabel="ClearImagePreviewTableViewCell" customClass="CustomTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="clearImagePreviewTableViewCell" selectionStyle="default" indentationWidth="10" reuseIdentifier="ClearDownloadStoreTableViewCell" id="BNU-sl-iSH" userLabel="ClearImagePreviewTableViewCell" customClass="CustomTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="495.5" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="BNU-sl-iSH" id="Rhf-1K-n9A">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Clear cache" textAlignment="natural" lineBreakMode="wordWrap" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sCy-8x-KOF">
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Clear download cache" textAlignment="natural" lineBreakMode="wordWrap" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sCy-8x-KOF">
|
||||
<rect key="frame" x="20" y="11" width="374" height="22"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="accountName"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
|
@ -1150,41 +1153,8 @@
|
|||
<outlet property="switchView" destination="PRY-Rw-kCQ" id="amr-y6-yCU"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="EnableNewUITableViewCell" selectionStyle="default" indentationWidth="10" reuseIdentifier="EnableNewUITableViewCell" id="wqW-gF-BK8" userLabel="EnableNewUITableViewCell" customClass="SwitchTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="143.5" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="wqW-gF-BK8" id="M3O-dZ-rUE">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="New UI" textAlignment="natural" lineBreakMode="wordWrap" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="n3F-rS-8OD">
|
||||
<rect key="frame" x="20" y="11.5" width="55.5" height="21"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="accountName"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="xts-GH-wU0">
|
||||
<rect key="frame" x="345" y="6.5" width="51" height="31"/>
|
||||
<connections>
|
||||
<action selector="valueChanged:" destination="wqW-gF-BK8" eventType="valueChanged" id="hXy-MR-STS"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="xts-GH-wU0" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="n3F-rS-8OD" secondAttribute="trailing" constant="8" id="28z-Gj-wRv"/>
|
||||
<constraint firstItem="xts-GH-wU0" firstAttribute="centerY" secondItem="M3O-dZ-rUE" secondAttribute="centerY" id="93A-SF-YTj"/>
|
||||
<constraint firstItem="n3F-rS-8OD" firstAttribute="centerY" secondItem="M3O-dZ-rUE" secondAttribute="centerY" id="XSg-3v-OkN"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="xts-GH-wU0" secondAttribute="trailing" id="hug-nf-sqx"/>
|
||||
<constraint firstAttribute="leadingMargin" secondItem="n3F-rS-8OD" secondAttribute="leading" id="zCO-rb-jKX"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="switchView" destination="xts-GH-wU0" id="gvk-zA-qhx"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="EnableMarkdownTableViewCell" selectionStyle="default" indentationWidth="10" reuseIdentifier="EnableMarkdownTableViewCell" id="GN8-OH-Bie" userLabel="EnableMarkdownTableViewCell" customClass="SwitchTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="187.5" width="414" height="44"/>
|
||||
<rect key="frame" x="0.0" y="143.5" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="GN8-OH-Bie" id="vwH-M8-Zdt">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
|
||||
|
@ -1217,7 +1187,7 @@
|
|||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" restorationIdentifier="EnableEmoticonsTableViewCell" selectionStyle="default" indentationWidth="10" reuseIdentifier="EnableEmoticonsTableViewCell" id="hfv-4K-f0U" userLabel="EnableEmoticonsTableViewCell" customClass="SwitchTableViewCell" customModule="Siskin" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="231.5" width="414" height="44"/>
|
||||
<rect key="frame" x="0.0" y="187.5" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="hfv-4K-f0U" id="Zpe-wG-sQi">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
|
||||
|
@ -1260,5 +1230,15 @@
|
|||
</objects>
|
||||
<point key="canvasLocation" x="1839" y="1742"/>
|
||||
</scene>
|
||||
<!--AboutController-->
|
||||
<scene sceneID="5Mf-Za-3AY">
|
||||
<objects>
|
||||
<viewControllerPlaceholder storyboardName="Info" referencedIdentifier="AboutController" id="5B4-ae-wmU" sceneMemberID="viewController">
|
||||
<navigationItem key="navigationItem" id="XxE-3L-zjI"/>
|
||||
</viewControllerPlaceholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="f6S-Ox-AWU" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2050" y="2202"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
|
598
SiskinIM/chats/AttachmentChatTableViewCell.swift
Normal file
598
SiskinIM/chats/AttachmentChatTableViewCell.swift
Normal file
|
@ -0,0 +1,598 @@
|
|||
//
|
||||
// AttachmentChatTableViewCell.swift
|
||||
//
|
||||
// Siskin IM
|
||||
// Copyright (C) 2019 "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 UIKit
|
||||
import MobileCoreServices
|
||||
import LinkPresentation
|
||||
import TigaseSwift
|
||||
|
||||
class AttachmentChatTableViewCell: BaseChatTableViewCell, UIContextMenuInteractionDelegate {
|
||||
|
||||
@IBOutlet var customView: UIView!;
|
||||
|
||||
fileprivate var tapGestureRecognizer: UITapGestureRecognizer?;
|
||||
fileprivate var longPressGestureRecognizer: UILongPressGestureRecognizer?;
|
||||
|
||||
private var item: ChatAttachment?;
|
||||
|
||||
private var linkView: UIView? {
|
||||
didSet {
|
||||
if let old = oldValue, let new = linkView {
|
||||
guard old != new else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let view = oldValue {
|
||||
view.removeFromSuperview();
|
||||
}
|
||||
if let view = linkView {
|
||||
self.customView.addSubview(view);
|
||||
if #available(iOS 13.0, *) {
|
||||
view.addInteraction(UIContextMenuInteraction(delegate: self));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib();
|
||||
tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGestureDidFire));
|
||||
tapGestureRecognizer?.cancelsTouchesInView = false;
|
||||
tapGestureRecognizer?.numberOfTapsRequired = 2;
|
||||
customView.addGestureRecognizer(tapGestureRecognizer!);
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
customView.addInteraction(UIContextMenuInteraction(delegate: self));
|
||||
customView.overrideUserInterfaceStyle = Appearance.current!.isDark ? .dark : .light;
|
||||
} else {
|
||||
longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressGestureDidFire));
|
||||
longPressGestureRecognizer?.cancelsTouchesInView = true;
|
||||
longPressGestureRecognizer?.delegate = self;
|
||||
|
||||
customView.addGestureRecognizer(longPressGestureRecognizer!);
|
||||
}
|
||||
}
|
||||
|
||||
func set(attachment item: ChatAttachment) {
|
||||
self.item = item;
|
||||
|
||||
super.set(item: item);
|
||||
|
||||
if let localUrl = DownloadStore.instance.url(for: "\(item.id)") {
|
||||
documentController = UIDocumentInteractionController(url: localUrl);
|
||||
if #available(iOS 13.0, *) {
|
||||
//self.longPressGestureRecognizer?.isEnabled = false;
|
||||
var metadata = MetadataCache.instance.metadata(for: "\(item.id)");
|
||||
let isNew = metadata == nil;
|
||||
if metadata == nil {
|
||||
metadata = LPLinkMetadata();
|
||||
metadata!.originalURL = localUrl;
|
||||
} else {
|
||||
metadata!.originalURL = nil;
|
||||
//metadata!.url = nil;
|
||||
//metadata!.title = "";
|
||||
//metadata!.originalURL = localUrl;
|
||||
metadata!.url = localUrl;
|
||||
}
|
||||
|
||||
let linkView = /*(self.linkView as? LPLinkView) ??*/ LPLinkView(metadata: metadata!);
|
||||
linkView.overrideUserInterfaceStyle = Appearance.current.isDark ? .dark :.light;
|
||||
linkView.setContentHuggingPriority(.defaultHigh, for: .vertical);
|
||||
linkView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical);
|
||||
linkView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal);
|
||||
linkView.translatesAutoresizingMaskIntoConstraints = false;
|
||||
|
||||
self.linkView = linkView;
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
linkView.topAnchor.constraint(equalTo: self.customView.topAnchor, constant: 0),
|
||||
linkView.bottomAnchor.constraint(equalTo: self.customView.bottomAnchor, constant: 0),
|
||||
linkView.leadingAnchor.constraint(equalTo: self.customView.leadingAnchor, constant: 0),
|
||||
linkView.trailingAnchor.constraint(equalTo: self.customView.trailingAnchor, constant: 0),
|
||||
linkView.heightAnchor.constraint(lessThanOrEqualToConstant: 350)
|
||||
]);
|
||||
|
||||
if isNew {
|
||||
MetadataCache.instance.generateMetadata(for: localUrl, withId: "\(item.id)", completionHandler: { meta1 in
|
||||
guard meta1 != nil else {
|
||||
return;
|
||||
}
|
||||
DBChatHistoryStore.instance.updateItem(for: item.account, with: item.jid, id: item.id, updateAppendix: { appendix in
|
||||
});
|
||||
})
|
||||
}
|
||||
} else {
|
||||
let attachmentInfo = (self.linkView as? AttachmentInfoView) ?? AttachmentInfoView(frame: .zero);
|
||||
attachmentInfo.backgroundColor = self.backgroundColor;
|
||||
attachmentInfo.isOpaque = true;
|
||||
|
||||
self.linkView = attachmentInfo;
|
||||
|
||||
//attachmentInfo.cellView = self;
|
||||
NSLayoutConstraint.activate([
|
||||
customView.leadingAnchor.constraint(equalTo: attachmentInfo.leadingAnchor),
|
||||
customView.trailingAnchor.constraint(greaterThanOrEqualTo: attachmentInfo.trailingAnchor),
|
||||
customView.topAnchor.constraint(equalTo: attachmentInfo.topAnchor),
|
||||
customView.bottomAnchor.constraint(equalTo: attachmentInfo.bottomAnchor)
|
||||
])
|
||||
attachmentInfo.set(item: item);
|
||||
}
|
||||
} else {
|
||||
documentController = nil;
|
||||
|
||||
let attachmentInfo = (self.linkView as? AttachmentInfoView) ?? AttachmentInfoView(frame: .zero);
|
||||
attachmentInfo.backgroundColor = self.backgroundColor;
|
||||
attachmentInfo.isOpaque = true;
|
||||
|
||||
//attachmentInfo.cellView = self;
|
||||
self.linkView = attachmentInfo;
|
||||
NSLayoutConstraint.activate([
|
||||
customView.leadingAnchor.constraint(equalTo: attachmentInfo.leadingAnchor),
|
||||
customView.trailingAnchor.constraint(greaterThanOrEqualTo: attachmentInfo.trailingAnchor),
|
||||
customView.topAnchor.constraint(equalTo: attachmentInfo.topAnchor),
|
||||
customView.bottomAnchor.constraint(equalTo: attachmentInfo.bottomAnchor)
|
||||
])
|
||||
attachmentInfo.set(item: item);
|
||||
|
||||
switch item.appendix.state {
|
||||
case .new:
|
||||
let sizeLimit = Settings.fileDownloadSizeLimit.integer();
|
||||
if sizeLimit > 0 {
|
||||
if let sessionObject = XmppService.instance.getClient(for: item.account)?.sessionObject, (RosterModule.getRosterStore(sessionObject).get(for: JID(item.jid))?.subscription ?? .none).isFrom || (DBChatStore.instance.getChat(for: item.account, with: item.jid) as? Room != nil) {
|
||||
DownloadManager.instance.download(item: item, maxSize: Int64(sizeLimit * 1024 * 1024));
|
||||
attachmentInfo.progress(show: true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
attachmentInfo.progress(show: DownloadManager.instance.downloadInProgress(for: item));
|
||||
default:
|
||||
attachmentInfo.progress(show: DownloadManager.instance.downloadInProgress(for: item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||
var cfg = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { suggestedActions -> UIMenu? in
|
||||
return self.prepareContextMenu();
|
||||
};
|
||||
return cfg;
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
func prepareContextMenu() -> UIMenu {
|
||||
guard let item = self.item else {
|
||||
return UIMenu(title: "");
|
||||
}
|
||||
|
||||
if let localUrl = DownloadStore.instance.url(for: "\(item.id)") {
|
||||
let items = [
|
||||
UIAction(title: "Preview", image: UIImage(systemName: "eye.fill"), handler: { action in
|
||||
print("preview called");
|
||||
self.open(url: localUrl, preview: true);
|
||||
}),
|
||||
UIAction(title: "Copy", image: UIImage(systemName: "doc.on.doc"), handler: { action in
|
||||
guard let text = self.item?.copyText(withTimestamp: Settings.CopyMessagesWithTimestamps.getBool(), withSender: false) else {
|
||||
return;
|
||||
}
|
||||
UIPasteboard.general.strings = [text];
|
||||
UIPasteboard.general.string = text;
|
||||
}),
|
||||
UIAction(title: "Share..", image: UIImage(systemName: "square.and.arrow.up"), handler: { action in
|
||||
print("share called");
|
||||
self.open(url: localUrl, preview: false);
|
||||
}),
|
||||
UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: [.destructive], handler: { action in
|
||||
print("delete called");
|
||||
DownloadStore.instance.deleteFile(for: "\(item.id)");
|
||||
DBChatHistoryStore.instance.updateItem(for: item.account, with: item.jid, id: item.id, updateAppendix: { appendix in
|
||||
appendix.state = .removed;
|
||||
})
|
||||
}),
|
||||
UIAction(title: "More..", image: UIImage(systemName: "ellipsis"), handler: { action in
|
||||
NotificationCenter.default.post(name: Notification.Name("tableViewCellShowEditToolbar"), object: self);
|
||||
})
|
||||
];
|
||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: items);
|
||||
} else {
|
||||
let items = [
|
||||
UIAction(title: "Copy", image: UIImage(systemName: "doc.on.doc"), handler: { action in
|
||||
guard let text = self.item?.copyText(withTimestamp: Settings.CopyMessagesWithTimestamps.getBool(), withSender: false) else {
|
||||
return;
|
||||
}
|
||||
UIPasteboard.general.strings = [text];
|
||||
UIPasteboard.general.string = text;
|
||||
}),
|
||||
UIAction(title: "Download", image: UIImage(systemName: "square.and.arrow.down"), handler: { action in
|
||||
print("download called");
|
||||
self.download(for: item);
|
||||
}),
|
||||
UIAction(title: "More..", image: UIImage(systemName: "ellipsis"), handler: { action in
|
||||
NotificationCenter.default.post(name: Notification.Name("tableViewCellShowEditToolbar"), object: self);
|
||||
})
|
||||
];
|
||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: items);
|
||||
}
|
||||
}
|
||||
|
||||
@objc func longPressGestureDidFire(_ recognizer: UILongPressGestureRecognizer) {
|
||||
guard recognizer.state == .recognized else {
|
||||
return;
|
||||
}
|
||||
downloadOrOpen();
|
||||
}
|
||||
|
||||
@objc func tapGestureDidFire(_ recognizer: UITapGestureRecognizer) {
|
||||
downloadOrOpen();
|
||||
}
|
||||
|
||||
var documentController: UIDocumentInteractionController? {
|
||||
didSet {
|
||||
if let value = oldValue {
|
||||
for recognizer in value.gestureRecognizers {
|
||||
self.removeGestureRecognizer(recognizer)
|
||||
}
|
||||
}
|
||||
if let value = documentController {
|
||||
value.delegate = self;
|
||||
for recognizer in value.gestureRecognizers {
|
||||
self.addGestureRecognizer(recognizer)
|
||||
}
|
||||
}
|
||||
longPressGestureRecognizer?.isEnabled = documentController == nil;
|
||||
}
|
||||
}
|
||||
|
||||
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
|
||||
let rootViewController = ((UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController)!;
|
||||
if let viewController = rootViewController.presentingViewController {
|
||||
return viewController;
|
||||
}
|
||||
return rootViewController;
|
||||
}
|
||||
|
||||
func open(url: URL, preview: Bool) {
|
||||
print("opening a file:", url, "exists:", FileManager.default.fileExists(atPath: url.path));// "tmp:", tmpUrl);
|
||||
let documentController = UIDocumentInteractionController(url: url);
|
||||
documentController.delegate = self;
|
||||
print("detected uti:", documentController.uti, "for:", documentController.url);
|
||||
if preview && documentController.presentPreview(animated: true) {
|
||||
self.documentController = documentController;
|
||||
} else if documentController.presentOptionsMenu(from: CGRect.zero, in: self.customView, animated: true) {
|
||||
self.documentController = documentController;
|
||||
}
|
||||
}
|
||||
|
||||
func download(for item: ChatAttachment) {
|
||||
DownloadManager.instance.download(item: item, maxSize: Int64.max);
|
||||
(self.linkView as? AttachmentInfoView)?.progress(show: true);
|
||||
}
|
||||
|
||||
// func documentInteractionControllerViewForPreview(_ controller: UIDocumentInteractionController) -> UIView? {
|
||||
// return self;
|
||||
// }
|
||||
|
||||
// func documentInteractionControllerDidDismissOptionsMenu(_ controller: UIDocumentInteractionController) {
|
||||
// print("file sharing cancelled!");
|
||||
// self.documentController = nil;
|
||||
// }
|
||||
//
|
||||
// func documentInteractionController(_ controller: UIDocumentInteractionController, didEndSendingToApplication application: String?) {
|
||||
// print("file shared with:", application);
|
||||
// self.documentController = nil;
|
||||
// }
|
||||
|
||||
private func downloadOrOpen() {
|
||||
guard let item = self.item else {
|
||||
return;
|
||||
}
|
||||
if let localUrl = DownloadStore.instance.url(for: "\(item.id)") {
|
||||
// let tmpUrl = FileManager.default.temporaryDirectory.appendingPathComponent(localUrl.lastPathComponent);
|
||||
// try? FileManager.default.copyItem(at: localUrl, to: tmpUrl);
|
||||
open(url: localUrl, preview: true);
|
||||
} else {
|
||||
let alert = UIAlertController(title: "Download", message: "File is not available locally. Should it be downloaded?", preferredStyle: .alert);
|
||||
alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { (action) in
|
||||
self.download(for: item);
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: nil));
|
||||
if let controller = (UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController {
|
||||
controller.present(alert, animated: true, completion: nil);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AttachmentInfoView: UIView {
|
||||
|
||||
let iconView: ImageAttachmentPreview;
|
||||
let filename: UILabel;
|
||||
let details: UILabel;
|
||||
|
||||
private var viewType: ViewType = .none {
|
||||
didSet {
|
||||
guard viewType != oldValue else {
|
||||
return;
|
||||
}
|
||||
switch oldValue {
|
||||
case .none:
|
||||
break;
|
||||
case .file:
|
||||
NSLayoutConstraint.deactivate(fileViewConstraints);
|
||||
case .imagePreview:
|
||||
NSLayoutConstraint.deactivate(imagePreviewConstraints);
|
||||
}
|
||||
switch viewType {
|
||||
case .none:
|
||||
break;
|
||||
case .file:
|
||||
NSLayoutConstraint.activate(fileViewConstraints);
|
||||
case .imagePreview:
|
||||
NSLayoutConstraint.activate(imagePreviewConstraints);
|
||||
}
|
||||
iconView.contentMode = viewType == .imagePreview ? .scaleAspectFill : .scaleAspectFit;
|
||||
iconView.isImagePreview = viewType == .imagePreview;
|
||||
}
|
||||
}
|
||||
|
||||
private var fileViewConstraints: [NSLayoutConstraint] = [];
|
||||
private var imagePreviewConstraints: [NSLayoutConstraint] = [];
|
||||
|
||||
override init(frame: CGRect) {
|
||||
iconView = ImageAttachmentPreview(frame: .zero);
|
||||
iconView.clipsToBounds = true
|
||||
iconView.image = UIImage(named: "defaultAvatar")!;
|
||||
iconView.translatesAutoresizingMaskIntoConstraints = false;
|
||||
iconView.setContentHuggingPriority(.defaultHigh, for: .vertical);
|
||||
iconView.setContentHuggingPriority(.defaultHigh, for: .horizontal);
|
||||
iconView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical);
|
||||
iconView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal);
|
||||
|
||||
filename = UILabel(frame: .zero);
|
||||
filename.font = UIFont.systemFont(ofSize: UIFont.systemFontSize - 1, weight: .semibold);
|
||||
filename.textColor = Appearance.current.labelColor;
|
||||
filename.translatesAutoresizingMaskIntoConstraints = false;
|
||||
filename.setContentHuggingPriority(.defaultHigh, for: .horizontal);
|
||||
filename.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal);
|
||||
|
||||
details = UILabel(frame: .zero);
|
||||
details.font = UIFont.systemFont(ofSize: UIFont.systemFontSize - 2, weight: .regular);
|
||||
details.textColor = Appearance.current.secondaryLabelColor;
|
||||
details.translatesAutoresizingMaskIntoConstraints = false;
|
||||
details.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
details.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal);
|
||||
|
||||
super.init(frame: frame);
|
||||
self.clipsToBounds = true
|
||||
self.translatesAutoresizingMaskIntoConstraints = false;
|
||||
self.isOpaque = false;
|
||||
|
||||
addSubview(iconView);
|
||||
addSubview(filename);
|
||||
addSubview(details);
|
||||
|
||||
fileViewConstraints = [
|
||||
iconView.heightAnchor.constraint(equalToConstant: 30),
|
||||
|
||||
iconView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 12),
|
||||
iconView.topAnchor.constraint(equalTo: self.topAnchor, constant: 8),
|
||||
iconView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8),
|
||||
|
||||
filename.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 12),
|
||||
filename.topAnchor.constraint(equalTo: self.topAnchor, constant: 8),
|
||||
filename.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor, constant: -12),
|
||||
|
||||
details.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 12),
|
||||
details.topAnchor.constraint(equalTo: filename.bottomAnchor, constant: 0),
|
||||
details.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8),
|
||||
// -- this is causing issue with progress indicatior!!
|
||||
details.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor, constant: -12),
|
||||
details.heightAnchor.constraint(equalTo: filename.heightAnchor)
|
||||
];
|
||||
|
||||
imagePreviewConstraints = [
|
||||
iconView.widthAnchor.constraint(lessThanOrEqualToConstant: 350),
|
||||
iconView.heightAnchor.constraint(lessThanOrEqualToConstant: 350),
|
||||
iconView.widthAnchor.constraint(lessThanOrEqualTo: self.widthAnchor),
|
||||
iconView.heightAnchor.constraint(lessThanOrEqualTo: self.widthAnchor),
|
||||
|
||||
iconView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
||||
iconView.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
iconView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
||||
|
||||
filename.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 12),
|
||||
filename.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 4),
|
||||
filename.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor, constant: -12),
|
||||
|
||||
details.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 12),
|
||||
details.topAnchor.constraint(equalTo: filename.bottomAnchor, constant: 0),
|
||||
details.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8),
|
||||
details.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor, constant: -12),
|
||||
details.heightAnchor.constraint(equalTo: filename.heightAnchor)
|
||||
];
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
let path = UIBezierPath(roundedRect: rect, cornerRadius: 10);
|
||||
path.addClip();
|
||||
Appearance.current.secondarySystemBackground.setFill();
|
||||
path.fill();
|
||||
|
||||
super.draw(rect);
|
||||
}
|
||||
|
||||
func set(item: ChatAttachment) {
|
||||
if let fileUrl = DownloadStore.instance.url(for: "\(item.id)") {
|
||||
filename.text = fileUrl.lastPathComponent;
|
||||
let fileSize = fileSizeToString(try! FileManager.default.attributesOfItem(atPath: fileUrl.path)[.size] as? UInt64);
|
||||
if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileUrl.pathExtension as CFString, nil)?.takeRetainedValue(), let typeName = UTTypeCopyDescription(uti)?.takeRetainedValue() as String? {
|
||||
details.text = "\(typeName) - \(fileSize)";
|
||||
if UTTypeConformsTo(uti, kUTTypeImage) {
|
||||
self.viewType = .imagePreview;
|
||||
print("preview of:" , fileUrl, fileUrl.path);
|
||||
iconView.image = UIImage(contentsOfFile: fileUrl.path)!;
|
||||
} else {
|
||||
self.viewType = .file;
|
||||
iconView.image = UIImage.icon(forFile: fileUrl, mimeType: item.appendix.mimetype);
|
||||
}
|
||||
} else {
|
||||
details.text = fileSize;
|
||||
iconView.image = UIImage.icon(forFile: fileUrl, mimeType: item.appendix.mimetype);
|
||||
self.viewType = .file;
|
||||
}
|
||||
} else {
|
||||
let filename = item.appendix.filename ?? URL(string: item.url)?.lastPathComponent ?? "";
|
||||
if filename.isEmpty {
|
||||
self.filename.text = "Unknown file";
|
||||
} else {
|
||||
self.filename.text = filename;
|
||||
}
|
||||
if let size = item.appendix.filesize {
|
||||
if let mimetype = item.appendix.mimetype, let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimetype as CFString, nil)?.takeRetainedValue(), let typeName = UTTypeCopyDescription(uti)?.takeRetainedValue() as String? {
|
||||
let fileSize = fileSizeToString(UInt64(size));
|
||||
details.text = "\(typeName) - \(fileSize)";
|
||||
iconView.image = UIImage.icon(forUTI: uti as String);
|
||||
} else {
|
||||
details.text = fileSizeToString(UInt64(size));
|
||||
iconView.image = UIImage.icon(forUTI: "public.content");
|
||||
}
|
||||
} else {
|
||||
details.text = "--";
|
||||
iconView.image = UIImage.icon(forUTI: "public.content");
|
||||
}
|
||||
self.viewType = .file;
|
||||
}
|
||||
}
|
||||
|
||||
var progressView: UIActivityIndicatorView?;
|
||||
|
||||
func progress(show: Bool) {
|
||||
guard show != (progressView != nil) else {
|
||||
return;
|
||||
}
|
||||
|
||||
if show {
|
||||
let view = UIActivityIndicatorView(style: .gray);
|
||||
view.translatesAutoresizingMaskIntoConstraints = false;
|
||||
view.color = Appearance.current.tintColor;
|
||||
self.addSubview(view);
|
||||
NSLayoutConstraint.activate([
|
||||
view.leadingAnchor.constraint(greaterThanOrEqualTo: filename.trailingAnchor, constant: 8),
|
||||
view.leadingAnchor.constraint(greaterThanOrEqualTo: details.trailingAnchor, constant: 8),
|
||||
view.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -12),
|
||||
view.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||
view.topAnchor.constraint(lessThanOrEqualTo: self.topAnchor)
|
||||
])
|
||||
self.progressView = view;
|
||||
view.startAnimating();
|
||||
} else if let view = progressView {
|
||||
view.stopAnimating();
|
||||
self.progressView = nil;
|
||||
view.removeFromSuperview();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func fileSizeToString(_ sizeIn: UInt64?) -> String {
|
||||
guard let size = sizeIn else {
|
||||
return "";
|
||||
}
|
||||
let formatter = ByteCountFormatter();
|
||||
formatter.countStyle = .file;
|
||||
return formatter.string(fromByteCount: Int64(size));
|
||||
}
|
||||
|
||||
enum ViewType {
|
||||
case none
|
||||
case file
|
||||
case imagePreview
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class ImageAttachmentPreview: UIImageView {
|
||||
|
||||
var isImagePreview: Bool = false {
|
||||
didSet {
|
||||
if isImagePreview != oldValue {
|
||||
if isImagePreview {
|
||||
self.layer.cornerRadius = 10;
|
||||
self.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner];
|
||||
} else {
|
||||
self.layer.cornerRadius = 0;
|
||||
self.layer.maskedCorners = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame);
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension FileManager {
|
||||
public func fileExtension(forUTI utiString: String) -> String? {
|
||||
guard
|
||||
let cfFileExtension = UTTypeCopyPreferredTagWithClass(utiString as CFString, kUTTagClassFilenameExtension)?.takeRetainedValue() else
|
||||
{
|
||||
return nil
|
||||
}
|
||||
|
||||
return cfFileExtension as String
|
||||
}
|
||||
}
|
||||
|
||||
extension UIImage {
|
||||
class func icon(forFile url: URL, mimeType: String?) -> UIImage? {
|
||||
let controller = UIDocumentInteractionController(url: url);
|
||||
if mimeType != nil, let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType! as CFString, nil)?.takeRetainedValue() as String? {
|
||||
controller.uti = uti;
|
||||
}
|
||||
if controller.icons.count == 0 {
|
||||
controller.uti = "public.data";
|
||||
}
|
||||
let icons = controller.icons;
|
||||
print("got:", icons.last, "for:", url.absoluteString);
|
||||
return icons.last;
|
||||
}
|
||||
|
||||
class func icon(forUTI utiString: String) -> UIImage? {
|
||||
let controller = UIDocumentInteractionController(url: URL(fileURLWithPath: "temp.file"));
|
||||
controller.uti = utiString;
|
||||
if controller.icons.count == 0 {
|
||||
controller.uti = "public.data";
|
||||
}
|
||||
let icons = controller.icons;
|
||||
print("got:", icons.last, "for:", utiString);
|
||||
return icons.last;
|
||||
}
|
||||
|
||||
}
|
159
SiskinIM/chats/BaseChatTableViewCell.swift
Normal file
159
SiskinIM/chats/BaseChatTableViewCell.swift
Normal file
|
@ -0,0 +1,159 @@
|
|||
//
|
||||
// BaseChatTableViewCell.swift
|
||||
//
|
||||
// Siskin IM
|
||||
// Copyright (C) 2019 "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 UIKit
|
||||
|
||||
class BaseChatTableViewCellFormatter {
|
||||
|
||||
fileprivate static let todaysFormatter = ({()-> DateFormatter in
|
||||
var f = DateFormatter();
|
||||
f.dateStyle = .none;
|
||||
f.timeStyle = .short;
|
||||
return f;
|
||||
})();
|
||||
fileprivate static let defaultFormatter = ({()-> DateFormatter in
|
||||
var f = DateFormatter();
|
||||
f.dateFormat = DateFormatter.dateFormat(fromTemplate: "dd.MM, jj:mm", options: 0, locale: NSLocale.current);
|
||||
// f.timeStyle = .NoStyle;
|
||||
return f;
|
||||
})();
|
||||
fileprivate static let fullFormatter = ({()-> DateFormatter in
|
||||
var f = DateFormatter();
|
||||
f.dateFormat = DateFormatter.dateFormat(fromTemplate: "dd.MM.yyyy, jj:mm", options: 0, locale: NSLocale.current);
|
||||
// f.timeStyle = .NoStyle;
|
||||
return f;
|
||||
})();
|
||||
|
||||
}
|
||||
|
||||
class BaseChatTableViewCell: UITableViewCell, UIDocumentInteractionControllerDelegate {
|
||||
|
||||
@IBOutlet var avatarView: AvatarView?
|
||||
@IBOutlet var nicknameView: UILabel?;
|
||||
@IBOutlet var timestampView: UILabel?
|
||||
@IBOutlet var stateView: UILabel?;
|
||||
|
||||
func formatTimestamp(_ ts: Date) -> String {
|
||||
let flags: Set<Calendar.Component> = [.day, .year];
|
||||
let components = Calendar.current.dateComponents(flags, from: ts, to: Date());
|
||||
if (components.day! < 1) {
|
||||
return BaseChatTableViewCellFormatter.todaysFormatter.string(from: ts);
|
||||
}
|
||||
if (components.year! != 0) {
|
||||
return BaseChatTableViewCellFormatter.fullFormatter.string(from: ts);
|
||||
} else {
|
||||
return BaseChatTableViewCellFormatter.defaultFormatter.string(from: ts);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
// Initialization code
|
||||
if avatarView != nil {
|
||||
avatarView!.layer.masksToBounds = true;
|
||||
avatarView!.layer.cornerRadius = avatarView!.frame.height / 2;
|
||||
}
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
if selected {
|
||||
let colors = contentView.subviews.map({ it -> UIColor in it.backgroundColor ?? UIColor.clear });
|
||||
super.setSelected(selected, animated: animated)
|
||||
selectedBackgroundView = UIView();
|
||||
contentView.subviews.enumerated().forEach { (offset, view) in
|
||||
if view .responds(to: #selector(setHighlighted(_:animated:))) {
|
||||
view.setValue(false, forKey: "highlighted");
|
||||
}
|
||||
print("offset", offset, "view", view);
|
||||
view.backgroundColor = colors[offset];
|
||||
}
|
||||
} else {
|
||||
super.setSelected(selected, animated: animated);
|
||||
selectedBackgroundView = nil;
|
||||
}
|
||||
// Configure the view for the selected state
|
||||
}
|
||||
|
||||
|
||||
func set(item: ChatViewItemProtocol) {
|
||||
var timestamp = formatTimestamp(item.timestamp);
|
||||
switch item.encryption {
|
||||
case .decrypted, .notForThisDevice, .decryptionFailed:
|
||||
timestamp = "\u{1F512} \(timestamp)";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch item.state {
|
||||
case .incoming_error, .incoming_error_unread:
|
||||
//elf.message.textColor = NSColor.systemRed;
|
||||
self.stateView?.text = "\u{203c}";
|
||||
case .outgoing_unsent:
|
||||
//self.message.textColor = NSColor.secondaryLabelColor;
|
||||
self.stateView?.text = "\u{1f4e4}";
|
||||
case .outgoing_delivered:
|
||||
//self.message.textColor = nil;
|
||||
self.stateView?.text = "\u{2713}";
|
||||
case .outgoing_error, .outgoing_error_unread:
|
||||
//self.message.textColor = nil;
|
||||
self.stateView?.text = "\u{203c}";
|
||||
default:
|
||||
//self.state?.stringValue = "";
|
||||
self.stateView?.text = nil;//NSColor.textColor;
|
||||
}
|
||||
if stateView == nil {
|
||||
if item.state.direction == .outgoing {
|
||||
timestampView?.textColor = UIColor.lightGray;
|
||||
if item.state.isError {
|
||||
timestampView?.textColor = UIColor.red;
|
||||
timestamp = "\(timestamp) Not delivered\u{203c}";
|
||||
} else if item.state == .outgoing_delivered {
|
||||
timestamp = "\(timestamp) \u{2713}";
|
||||
}
|
||||
}
|
||||
}
|
||||
timestampView?.text = timestamp;
|
||||
|
||||
self.nicknameView?.textColor = Appearance.current.labelColor;
|
||||
|
||||
if item.state.isError {
|
||||
if item.state.direction == .outgoing {
|
||||
self.accessoryType = .detailButton;
|
||||
self.tintColor = UIColor.red;
|
||||
}
|
||||
} else {
|
||||
self.accessoryType = .none;
|
||||
self.tintColor = stateView?.tintColor;
|
||||
}
|
||||
|
||||
self.stateView?.textColor = item.state.isError && item.state.direction == .incoming ? UIColor.red : Appearance.current.labelColor;
|
||||
self.timestampView?.textColor = item.state.isError && item.state.direction == .incoming ? UIColor.red : Appearance.current.labelColor;
|
||||
}
|
||||
|
||||
@objc func actionMore(_ sender: UIMenuController) {
|
||||
NotificationCenter.default.post(name: NSNotification.Name("tableViewCellShowEditToolbar"), object: self);
|
||||
}
|
||||
|
||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
return super.canPerformAction(action, withSender: sender) || action == #selector(actionMore(_:));
|
||||
}
|
||||
|
||||
}
|
|
@ -140,7 +140,7 @@ class BaseChatViewController: UIViewController, UITextViewDelegate, UITableViewD
|
|||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
NotificationCenter.default.removeObserver(self);
|
||||
//NotificationCenter.default.removeObserver(self);
|
||||
super.viewDidDisappear(animated);
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,8 @@ class BaseChatViewControllerWithDataSource: BaseChatViewController, ChatViewData
|
|||
|
||||
let firstRowIndexPath = IndexPath(row: 0, section: 0);
|
||||
|
||||
private var loaded: Bool = false;
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad();
|
||||
dataSource.delegate = self;
|
||||
|
@ -36,10 +38,13 @@ class BaseChatViewControllerWithDataSource: BaseChatViewController, ChatViewData
|
|||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated);
|
||||
|
||||
self.dataSource.refreshData(unread: chat.unread) { (firstUnread) in
|
||||
print("got first unread at:", firstUnread);
|
||||
if self.tableView.numberOfRows(inSection: 0) > 0 {
|
||||
self.tableView.scrollToRow(at: IndexPath(row: firstUnread ?? 0, section: 0), at: .none, animated: true);
|
||||
if !loaded {
|
||||
loaded = true;
|
||||
self.dataSource.refreshData(unread: chat.unread) { (firstUnread) in
|
||||
print("got first unread at:", firstUnread);
|
||||
if self.tableView.numberOfRows(inSection: 0) > 0 {
|
||||
self.tableView.scrollToRow(at: IndexPath(row: firstUnread ?? 0, section: 0), at: .none, animated: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,11 +40,15 @@ class BaseChatViewControllerWithDataSourceAndContextMenuAndToolbar: BaseChatView
|
|||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
bottomViewHeightConstraint?.isActive = false;
|
||||
var items: [UIMenuItem] = UIMenuController.shared.menuItems ?? [];
|
||||
items.append(UIMenuItem(title: "More..", action: #selector(ChatTableViewCell.actionMore(_:))));
|
||||
UIMenuController.shared.menuItems = items;
|
||||
if #available(iOS 13.0, *) {
|
||||
|
||||
} else {
|
||||
var items: [UIMenuItem] = UIMenuController.shared.menuItems ?? [];
|
||||
items.append(UIMenuItem(title: "More..", action: #selector(ChatTableViewCell.actionMore(_:))));
|
||||
UIMenuController.shared.menuItems = items;
|
||||
|
||||
customToolbar?.barStyle = Appearance.current.isDark ? .black : .default;
|
||||
customToolbar?.barStyle = Appearance.current.isDark ? .black : .default;
|
||||
}
|
||||
|
||||
super.viewWillAppear(animated);
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(showEditToolbar), name: NSNotification.Name("tableViewCellShowEditToolbar"), object: nil);
|
||||
|
@ -71,6 +75,8 @@ class BaseChatViewControllerWithDataSourceAndContextMenuAndToolbar: BaseChatView
|
|||
let timestampsSwitch = UIBarButtonItem(title: "Timestamps: \(self.withTimestamps ? "ON" : "OFF")", style: .plain, target: self, action: #selector(BaseChatViewControllerWithDataSourceAndContextMenuAndToolbar.switchWithTimestamps));
|
||||
self.timestampsSwitch = timestampsSwitch;
|
||||
|
||||
self.updateTimestampSwitch();
|
||||
|
||||
let items = [
|
||||
timestampsSwitch,
|
||||
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
|
||||
|
@ -113,7 +119,18 @@ class BaseChatViewControllerWithDataSourceAndContextMenuAndToolbar: BaseChatView
|
|||
|
||||
@objc func switchWithTimestamps() {
|
||||
withTimestamps = !withTimestamps;
|
||||
timestampsSwitch?.title = "Timestamps: \(withTimestamps ? "ON" : "OFF")";
|
||||
updateTimestampSwitch();
|
||||
|
||||
}
|
||||
|
||||
private func updateTimestampSwitch() {
|
||||
if #available(iOS 13.0, *) {
|
||||
timestampsSwitch?.image = UIImage(systemName: withTimestamps ? "clock.fill" : "clock");
|
||||
timestampsSwitch?.title = nil;
|
||||
} else {
|
||||
timestampsSwitch?.title = "Timestamps: \(withTimestamps ? "ON" : "OFF")";
|
||||
timestampsSwitch?.image = nil;
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func copyMessageInt(paths: [IndexPath]) {
|
||||
|
@ -132,17 +149,24 @@ class BaseChatViewControllerWithDataSourceAndContextMenuAndToolbar: BaseChatView
|
|||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
|
||||
if #available(iOS 13.0, *) {
|
||||
} else {
|
||||
if action == #selector(UIResponderStandardEditActions.copy(_:)) {
|
||||
return true;
|
||||
}
|
||||
if customToolbar != nil && action == #selector(ChatTableViewCell.actionMore(_:)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
|
||||
return true;
|
||||
if #available(iOS 13.0, *) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) {
|
||||
|
@ -153,46 +177,22 @@ class BaseChatViewControllerWithDataSourceAndContextMenuAndToolbar: BaseChatView
|
|||
}
|
||||
|
||||
func getTextOfSelectedRows(paths: [IndexPath], withTimestamps: Bool, handler: (([String]) -> Void)?) {
|
||||
let items: [ChatMessage] = paths.map({ index in dataSource.getItem(at: index.row) }).map({ it -> ChatMessage? in
|
||||
return it as? ChatMessage;
|
||||
}).filter({ it -> Bool in it != nil }).map({ it -> ChatMessage in return it! }).sorted { (it1, it2) -> Bool in
|
||||
let items: [ChatViewItemProtocol] = paths.map({ index in dataSource.getItem(at: index.row)! }).sorted { (it1, it2) -> Bool in
|
||||
it1.timestamp.compare(it2.timestamp) == .orderedAscending;
|
||||
};
|
||||
|
||||
guard items.count > 1 else {
|
||||
let texts = items.map({ (it) -> String in
|
||||
return it.message;
|
||||
});
|
||||
handler?(texts);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let withoutPrefix = Set(items.map({it in it.state.direction})).count == 1;
|
||||
|
||||
let formatter = DateFormatter();
|
||||
formatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "dd.MM.yyyy jj:mm", options: 0, locale: NSLocale.current);
|
||||
|
||||
var direction: MessageDirection? = nil;
|
||||
let rosterModule: RosterModule? = XmppService.instance.getClient(for: self.account)?.modulesManager.getModule(RosterModule.ID);
|
||||
let rosterStore = rosterModule?.rosterStore;
|
||||
let texts = items.map { (it) -> String in
|
||||
if withoutPrefix {
|
||||
if withTimestamps {
|
||||
return "[\(formatter.string(from: it.timestamp))] \(it.message)";
|
||||
} else {
|
||||
return it.message;
|
||||
}
|
||||
} else {
|
||||
let prefix = (direction == nil || it.state.direction != direction!) ?
|
||||
"\(it.state.direction == .incoming ? (it.authorNickname ?? rosterStore?.get(for: JID(it.jid))?.name ?? chat.jid.localPart ?? chat.jid.domain) : "Me"):\n" : "";
|
||||
direction = it.state.direction;
|
||||
if withTimestamps {
|
||||
return "\(prefix) [\(formatter.string(from: it.timestamp))] \(it.message ?? "")"
|
||||
} else {
|
||||
return "\(prefix) \(it.message ?? "")"
|
||||
}
|
||||
}
|
||||
}
|
||||
let texts = items.map { (it) -> String? in
|
||||
return it.copyText(withTimestamp: withTimestamps, withSender: !withoutPrefix);
|
||||
}.filter { (text) -> Bool in
|
||||
return text != nil;
|
||||
}.map { (text) -> String in
|
||||
return text!;
|
||||
};
|
||||
|
||||
print("got texts", texts);
|
||||
handler?(texts);
|
||||
|
|
|
@ -28,63 +28,63 @@ protocol BaseChatViewController_PreviewExtension {
|
|||
|
||||
extension BaseChatViewController_PreviewExtension {
|
||||
|
||||
func downloadPreview(url: URL, msgId: Int, account: BareJID, jid: BareJID) {
|
||||
guard Settings.MaxImagePreviewSize.getInt() != 0 else {
|
||||
return;
|
||||
}
|
||||
|
||||
getHeaders(url: url) { (mimeType, size, errCode) in
|
||||
if mimeType?.hasPrefix("image/") ?? false && (size ?? Int64.max) < self.getImageDownloadSizeLimit() {
|
||||
self.downloadImageFile(url: url, completion: { (key) in
|
||||
let previewKey = key == nil ? nil : "preview:image:\(key!)";
|
||||
DBChatHistoryStore.instance.updateItem(for: account, with: jid, id: msgId, preview: previewKey ?? "");
|
||||
})
|
||||
} else {
|
||||
DBChatHistoryStore.instance.updateItem(for: account, with: jid, id: msgId, preview: "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getHeaders(url: URL, completion: @escaping (String?, Int64?, Int)->Void) {
|
||||
var request = URLRequest(url: url);
|
||||
request.httpMethod = "HEAD";
|
||||
URLSession.shared.dataTask(with: request) { (data, resp, error) in
|
||||
let response = resp as? HTTPURLResponse;
|
||||
print("got mime type =", response?.mimeType as Any, "with size", response?.expectedContentLength as Any, "at", url);
|
||||
// func downloadPreview(url: URL, msgId: Int, account: BareJID, jid: BareJID) {
|
||||
// guard Settings.MaxImagePreviewSize.getInt() != 0 else {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// getHeaders(url: url) { (mimeType, size, errCode) in
|
||||
// if mimeType?.hasPrefix("image/") ?? false && (size ?? Int64.max) < self.getImageDownloadSizeLimit() {
|
||||
// self.downloadImageFile(url: url, completion: { (key) in
|
||||
// let previewKey = key == nil ? nil : "preview:image:\(key!)";
|
||||
// DBChatHistoryStore.instance.updateItem(for: account, with: jid, id: msgId, preview: previewKey ?? "");
|
||||
// })
|
||||
// } else {
|
||||
// DBChatHistoryStore.instance.updateItem(for: account, with: jid, id: msgId, preview: "");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func getHeaders(url: URL, completion: @escaping (String?, Int64?, Int)->Void) {
|
||||
// var request = URLRequest(url: url);
|
||||
// request.httpMethod = "HEAD";
|
||||
// URLSession.shared.dataTask(with: request) { (data, resp, error) in
|
||||
// let response = resp as? HTTPURLResponse;
|
||||
// print("got mime type =", response?.mimeType as Any, "with size", response?.expectedContentLength as Any, "at", url);
|
||||
//
|
||||
// completion(response?.mimeType, response?.expectedContentLength, response?.statusCode ?? 500);
|
||||
// }.resume();
|
||||
// }
|
||||
|
||||
completion(response?.mimeType, response?.expectedContentLength, response?.statusCode ?? 500);
|
||||
}.resume();
|
||||
}
|
||||
|
||||
fileprivate func downloadImageFile(url: URL, completion: @escaping (String?)->Void) {
|
||||
URLSession.shared.downloadTask(with: url, completionHandler: { (tmpUrl, resp, error) in
|
||||
print("downloaded content of", url, "to", tmpUrl as Any, "response:", resp as Any, "error:", error as Any);
|
||||
guard let response = resp as? HTTPURLResponse else {
|
||||
completion(nil);
|
||||
return;
|
||||
}
|
||||
guard tmpUrl != nil && error == nil else {
|
||||
completion(nil);
|
||||
return;
|
||||
}
|
||||
|
||||
if let image = UIImage(contentsOfFile: tmpUrl!.path) {
|
||||
print("loaded image", url, "size", image.size);
|
||||
ImageCache.shared.set(url: tmpUrl!, mimeType: response.mimeType) { (key) in
|
||||
completion(key);
|
||||
}
|
||||
} else {
|
||||
completion(nil);
|
||||
}
|
||||
}).resume();
|
||||
}
|
||||
|
||||
func getImageDownloadSizeLimit() -> Int64 {
|
||||
let val = Settings.MaxImagePreviewSize.getInt();
|
||||
if val > (Int.max / (1024*1024)) {
|
||||
return Int64(val);
|
||||
} else {
|
||||
return Int64(val * 1024 * 1024);
|
||||
}
|
||||
}
|
||||
// fileprivate func downloadImageFile(url: URL, completion: @escaping (String?)->Void) {
|
||||
// URLSession.shared.downloadTask(with: url, completionHandler: { (tmpUrl, resp, error) in
|
||||
// print("downloaded content of", url, "to", tmpUrl as Any, "response:", resp as Any, "error:", error as Any);
|
||||
// guard let response = resp as? HTTPURLResponse else {
|
||||
// completion(nil);
|
||||
// return;
|
||||
// }
|
||||
// guard tmpUrl != nil && error == nil else {
|
||||
// completion(nil);
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// if let image = UIImage(contentsOfFile: tmpUrl!.path) {
|
||||
// print("loaded image", url, "size", image.size);
|
||||
// ImageCache.shared.set(url: tmpUrl!, mimeType: response.mimeType) { (key) in
|
||||
// completion(key);
|
||||
// }
|
||||
// } else {
|
||||
// completion(nil);
|
||||
// }
|
||||
// }).resume();
|
||||
// }
|
||||
//
|
||||
// func getImageDownloadSizeLimit() -> Int64 {
|
||||
// let val = Settings.MaxImagePreviewSize.getInt();
|
||||
// if val > (Int.max / (1024*1024)) {
|
||||
// return Int64(val);
|
||||
// } else {
|
||||
// return Int64(val * 1024 * 1024);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -35,8 +35,9 @@ protocol BaseChatViewController_ShareImageExtension: class {
|
|||
var account: BareJID! { get }
|
||||
var jid: BareJID! { get }
|
||||
|
||||
func sendAttachment(originalUrl: URL?, uploadedUrl: String, appendix: ChatAttachmentAppendix, completionHandler: (()->Void)?);
|
||||
|
||||
func sendMessage(body: String, url: String?, preview: String?, completed: (()->Void)?);
|
||||
// func sendMessage(body: String, url: String?, preview: String?, completed: (()->Void)?);
|
||||
|
||||
func present(_ controller: UIViewController, animated: Bool, completion: (()->Void)?);
|
||||
}
|
||||
|
@ -124,7 +125,7 @@ class BaseChatViewController_SharePickerDelegate: NSObject, URLSessionDelegate,
|
|||
self.controller = controller;
|
||||
}
|
||||
|
||||
func share(filename: String, url: URL, completionHandler: @escaping (Result<URL,ShareError>)->Void) {
|
||||
func share(filename: String, url: URL, completionHandler: @escaping (UploadResult)->Void) {
|
||||
guard url.startAccessingSecurityScopedResource() else {
|
||||
completionHandler(.failure(.noAccessError));
|
||||
return;
|
||||
|
@ -135,11 +136,10 @@ class BaseChatViewController_SharePickerDelegate: NSObject, URLSessionDelegate,
|
|||
return;
|
||||
}
|
||||
|
||||
var mimeType: String = "application/octet-stream";
|
||||
var mimeType: String? = nil;
|
||||
|
||||
if let type = values.typeIdentifier, let mimeTypeRef = UTTypeCopyPreferredTagWithClass(type as CFString, kUTTagClassMIMEType) {
|
||||
mimeType = mimeTypeRef.takeUnretainedValue() as String;
|
||||
mimeTypeRef.release();
|
||||
if let type = values.typeIdentifier {
|
||||
mimeType = UTTypeCopyPreferredTagWithClass(type as CFString, kUTTagClassMIMEType)?.takeRetainedValue() as String?;
|
||||
}
|
||||
|
||||
guard let inputStream = InputStream(url: url) else {
|
||||
|
@ -147,11 +147,11 @@ class BaseChatViewController_SharePickerDelegate: NSObject, URLSessionDelegate,
|
|||
completionHandler(.failure(.noAccessError));
|
||||
return;
|
||||
}
|
||||
self.share(filename: filename, inputStream: inputStream, filesize: size, mimeType: mimeType, completionHandler: { result in
|
||||
self.share(filename: filename, inputStream: inputStream, filesize: size, mimeType: mimeType ?? "application/octet-stream", completionHandler: { result in
|
||||
url.stopAccessingSecurityScopedResource();
|
||||
switch result {
|
||||
case .success(let getUri):
|
||||
completionHandler(.success(getUri));
|
||||
completionHandler(.success(url: getUri, filesize: size, mimeType: mimeType));
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error));
|
||||
}
|
||||
|
@ -235,6 +235,12 @@ class BaseChatViewController_SharePickerDelegate: NSObject, URLSessionDelegate,
|
|||
self.controller.progressBar.progress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
enum UploadResult {
|
||||
case success(url: URL, filesize: Int, mimeType: String?)
|
||||
case failure(ShareError)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class BaseChatViewController_ShareFilePickerDelegate: BaseChatViewController_SharePickerDelegate, UIDocumentPickerDelegate {
|
||||
|
@ -249,9 +255,14 @@ class BaseChatViewController_ShareFilePickerDelegate: BaseChatViewController_Sha
|
|||
|
||||
share(filename: url.lastPathComponent, url: url) { (result) in
|
||||
switch result {
|
||||
case .success(let getUri):
|
||||
print("file uploaded to:", getUri);
|
||||
self.controller.sendMessage(body: getUri.absoluteString, url: getUri.absoluteString, preview: nil, completed: nil);
|
||||
case .success(let uploadedUrl, let filesize, let mimetype):
|
||||
print("file uploaded to:", uploadedUrl);
|
||||
var appendix = ChatAttachmentAppendix()
|
||||
appendix.filename = url.lastPathComponent;
|
||||
appendix.filesize = filesize;
|
||||
appendix.mimetype = mimetype;
|
||||
appendix.state = .downloaded;
|
||||
self.controller.sendAttachment(originalUrl: url, uploadedUrl: uploadedUrl.absoluteString, appendix: appendix, completionHandler: nil);
|
||||
case .failure(let error):
|
||||
self.showAlert(shareError: error);
|
||||
}
|
||||
|
@ -288,9 +299,25 @@ class BaseChatViewController_ShareImagePickerDelegate: BaseChatViewController_Sh
|
|||
switch result {
|
||||
case .success(let getUri):
|
||||
print("file uploaded to:", getUri);
|
||||
ImageCache.shared.set(image: photo) { (key) in
|
||||
self.controller.sendMessage(body: getUri.absoluteString, url: getUri.absoluteString, preview: "preview:image:\(key)", completed: nil);
|
||||
var appendix = ChatAttachmentAppendix()
|
||||
appendix.filename = "image.jpg";
|
||||
appendix.filesize = data!.count;
|
||||
appendix.mimetype = "image/jpeg";
|
||||
appendix.state = .downloaded;
|
||||
|
||||
var url: URL? = FileManager.default.temporaryDirectory.appendingPathComponent("image.jpg", isDirectory: false);
|
||||
do {
|
||||
try data!.write(to: url!);
|
||||
} catch {
|
||||
url = nil;
|
||||
}
|
||||
|
||||
self.controller.sendAttachment(originalUrl: url, uploadedUrl: getUri.absoluteString, appendix: appendix, completionHandler: {
|
||||
// attachment was sent..
|
||||
if let url = url, FileManager.default.fileExists(atPath: url.path) {
|
||||
try? FileManager.default.removeItem(at: url);
|
||||
}
|
||||
})
|
||||
case .failure(let error):
|
||||
self.showAlert(shareError: error);
|
||||
}
|
||||
|
|
128
SiskinIM/chats/ChatAttachementsCellView.swift
Normal file
128
SiskinIM/chats/ChatAttachementsCellView.swift
Normal file
|
@ -0,0 +1,128 @@
|
|||
//
|
||||
// ChatAttachementsCellView.swift
|
||||
// Siskin IM
|
||||
//
|
||||
// Created by Andrzej Wójcik on 03/01/2020.
|
||||
// Copyright © 2020 Tigase, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
import TigaseSwift
|
||||
|
||||
class ChatAttachmentsCellView: UICollectionViewCell, UIDocumentInteractionControllerDelegate, UIContextMenuInteractionDelegate {
|
||||
|
||||
@IBOutlet var imageField: UIImageView!
|
||||
|
||||
private var id: Int {
|
||||
return item?.id ?? NSNotFound;
|
||||
};
|
||||
private var item: ChatAttachment?;
|
||||
|
||||
func set(item: ChatAttachment) {
|
||||
self.item = item;
|
||||
|
||||
if #available(iOS 13.0, *), self.interactions.isEmpty {
|
||||
self.addInteraction(UIContextMenuInteraction(delegate: self));
|
||||
}
|
||||
|
||||
if let fileUrl = DownloadStore.instance.url(for: "\(item.id)") {
|
||||
if #available(iOS 13.0, *), let imageProvider = MetadataCache.instance.metadata(for: "\(item.id)")?.imageProvider {
|
||||
imageField.image = UIImage.icon(forFile: fileUrl, mimeType: nil);
|
||||
imageProvider.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil, completionHandler: { (data, error) in
|
||||
guard let data = data, error == nil else {
|
||||
return;
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
guard self.id == item.id else {
|
||||
return;
|
||||
}
|
||||
switch data {
|
||||
case let image as UIImage:
|
||||
self.imageField.image = image;
|
||||
case let data as Data:
|
||||
self.imageField.image = UIImage(data: data);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if let image = UIImage(contentsOfFile: fileUrl.path) {
|
||||
self.imageField.image = image;
|
||||
} else {
|
||||
self.imageField.image = UIImage.icon(forFile: fileUrl, mimeType: nil);
|
||||
}
|
||||
} else {
|
||||
if let mimetype = item.appendix.mimetype, let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimetype as CFString, nil)?.takeRetainedValue() as String? {
|
||||
imageField.image = UIImage.icon(forUTI: uti);
|
||||
} else {
|
||||
imageField.image = UIImage.icon(forUTI: "public.content")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { suggestedActions in
|
||||
return self.prepareContextMenu();
|
||||
})
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
func prepareContextMenu() -> UIMenu {
|
||||
guard let item = self.item else {
|
||||
return UIMenu(title: "");
|
||||
}
|
||||
|
||||
if let localUrl = DownloadStore.instance.url(for: "\(item.id)") {
|
||||
let items = [
|
||||
UIAction(title: "Preview", image: UIImage(systemName: "eye.fill"), handler: { action in
|
||||
print("preview called");
|
||||
self.open(url: localUrl, preview: true);
|
||||
}),
|
||||
UIAction(title: "Copy", image: UIImage(systemName: "doc.on.doc"), handler: { action in
|
||||
guard let text = self.item?.copyText(withTimestamp: Settings.CopyMessagesWithTimestamps.getBool(), withSender: false) else {
|
||||
return;
|
||||
}
|
||||
UIPasteboard.general.strings = [text];
|
||||
UIPasteboard.general.string = text;
|
||||
}),
|
||||
UIAction(title: "Share..", image: UIImage(systemName: "square.and.arrow.up"), handler: { action in
|
||||
print("share called");
|
||||
self.open(url: localUrl, preview: false);
|
||||
}),
|
||||
UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: [.destructive], handler: { action in
|
||||
print("delete called");
|
||||
DownloadStore.instance.deleteFile(for: "\(item.id)");
|
||||
DBChatHistoryStore.instance.updateItem(for: item.account, with: item.jid, id: item.id, updateAppendix: { appendix in
|
||||
appendix.state = .removed;
|
||||
})
|
||||
})
|
||||
];
|
||||
return UIMenu(title: localUrl.lastPathComponent, image: nil, identifier: nil, options: [], children: items);
|
||||
} else {
|
||||
return UIMenu(title: "");
|
||||
}
|
||||
}
|
||||
|
||||
var documentController: UIDocumentInteractionController?;
|
||||
|
||||
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
|
||||
let viewController = ((UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController?.presentedViewController)!;
|
||||
return viewController;
|
||||
}
|
||||
|
||||
func open(url: URL, preview: Bool) {
|
||||
print("opening a file:", url, "exists:", FileManager.default.fileExists(atPath: url.path));// "tmp:", tmpUrl);
|
||||
let documentController = UIDocumentInteractionController(url: url);
|
||||
documentController.delegate = self;
|
||||
documentController.name = url.lastPathComponent;
|
||||
print("detected uti:", documentController.uti, "for:", documentController.url);
|
||||
if preview && documentController.presentPreview(animated: true) {
|
||||
self.documentController = documentController;
|
||||
} else if documentController.presentOptionsMenu(from: CGRect.zero, in: self, animated: true) {
|
||||
self.documentController = documentController;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
140
SiskinIM/chats/ChatAttachementsController.swift
Normal file
140
SiskinIM/chats/ChatAttachementsController.swift
Normal file
|
@ -0,0 +1,140 @@
|
|||
//
|
||||
// ChatAttachementsController.swift
|
||||
// Siskin IM
|
||||
//
|
||||
// Created by Andrzej Wójcik on 03/01/2020.
|
||||
// Copyright © 2020 Tigase, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import TigaseSwift
|
||||
|
||||
class ChatAttachmentsController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
|
||||
|
||||
private var items: [ChatAttachment] = [];
|
||||
|
||||
var account: BareJID?;
|
||||
var jid: BareJID?;
|
||||
|
||||
private var loaded: Bool = false;
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad();
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(appearanceChanged), name: Appearance.CHANGED, object: nil);
|
||||
updateAppearance();
|
||||
if #available(iOS 13.0, *) {
|
||||
} else {
|
||||
self.view.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(gesture:))));
|
||||
}
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(messageUpdated), name: DBChatHistoryStore.MESSAGE_UPDATED, object: nil);
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated);
|
||||
self.updateAppearance();
|
||||
if let account = self.account, let jid = self.jid, !loaded {
|
||||
self.loaded = true;
|
||||
DBChatHistoryStore.instance.loadAttachments(for: account, with: jid, completionHandler: { attachments in
|
||||
DispatchQueue.main.async {
|
||||
self.items = attachments.filter({ (attachment) -> Bool in
|
||||
return DownloadStore.instance.url(for: "\(attachment.id)") != nil;
|
||||
});
|
||||
self.collectionView.reloadData();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override func numberOfSections(in collectionView: UICollectionView) -> Int {
|
||||
return 1;
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
if items.isEmpty {
|
||||
if self.collectionView.backgroundView == nil {
|
||||
let label = UILabel(frame: CGRect(x: 0, y:0, width: self.view.bounds.size.width, height: self.view.bounds.size.height));
|
||||
label.text = "No attachments";
|
||||
label.font = UIFont.systemFont(ofSize: UIFont.systemFontSize + 2, weight: .medium);
|
||||
label.numberOfLines = 0;
|
||||
label.textAlignment = .center;
|
||||
label.sizeToFit();
|
||||
label.textColor = Appearance.current.secondaryLabelColor;
|
||||
self.collectionView.backgroundView = label;
|
||||
}
|
||||
} else {
|
||||
self.collectionView.backgroundView = nil;
|
||||
}
|
||||
return items.count;
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = self.collectionView.dequeueReusableCell(withReuseIdentifier: "AttachmentCellView", for: indexPath) as! ChatAttachmentsCellView;
|
||||
cell.set(item: items[indexPath.item]);
|
||||
return cell;
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
let width = (self.view.bounds.width - 2 * 2.0) / 3.0;
|
||||
return CGSize(width: width, height: width);
|
||||
}
|
||||
|
||||
@objc func appearanceChanged(_ notification: Notification) {
|
||||
self.updateAppearance();
|
||||
}
|
||||
|
||||
func updateAppearance() {
|
||||
if #available(iOS 13.0, *) {
|
||||
overrideUserInterfaceStyle = Appearance.current.isDark ? .dark : .light;
|
||||
};
|
||||
self.view.tintColor = Appearance.current.tintColor;
|
||||
|
||||
self.collectionView.backgroundColor = Appearance.current.systemBackground;
|
||||
|
||||
if let navController = self.navigationController {
|
||||
navController.navigationBar.barStyle = Appearance.current.navigationBarStyle;
|
||||
navController.navigationBar.tintColor = Appearance.current.navigationBarTintColor;
|
||||
navController.navigationBar.barTintColor = Appearance.current.controlBackgroundColor;
|
||||
navController.navigationBar.setNeedsLayout();
|
||||
navController.navigationBar.layoutIfNeeded();
|
||||
navController.navigationBar.setNeedsDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
@objc func messageUpdated(_ notification: Notification) {
|
||||
DispatchQueue.main.async {
|
||||
guard let attachment = notification.object as? ChatAttachment, attachment.account == self.account, attachment.jid == self.jid else {
|
||||
return;
|
||||
}
|
||||
|
||||
guard let idx = self.items.firstIndex(where: { (att) -> Bool in
|
||||
return att.id == attachment.id;
|
||||
}) else {
|
||||
return;
|
||||
}
|
||||
|
||||
self.items.remove(at: idx);
|
||||
self.collectionView.deleteItems(at: [IndexPath(item: idx, section: 0)]);
|
||||
}
|
||||
}
|
||||
|
||||
var documentController: UIDocumentInteractionController?;
|
||||
|
||||
@objc func handleLongPress(gesture: UILongPressGestureRecognizer) {
|
||||
guard gesture.state == .recognized else {
|
||||
return;
|
||||
}
|
||||
|
||||
let p = gesture.location(in: self.collectionView);
|
||||
if let indexPath = self.collectionView.indexPathForItem(at: p) {
|
||||
let item = self.items[indexPath.row];
|
||||
if let url = DownloadStore.instance.url(for: "\(item.id)") {
|
||||
let documentController = UIDocumentInteractionController(url: url);
|
||||
//documentController.delegate = self;
|
||||
if documentController.presentOptionsMenu(from: CGRect.zero, in: self.collectionView, animated: true) {
|
||||
self.documentController = documentController;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
93
SiskinIM/chats/ChatAttachment.swift
Normal file
93
SiskinIM/chats/ChatAttachment.swift
Normal file
|
@ -0,0 +1,93 @@
|
|||
//
|
||||
// ChatAttachment.swift
|
||||
//
|
||||
// Siskin IM
|
||||
// Copyright (C) 2019 "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
|
||||
|
||||
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?, 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, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: error);
|
||||
}
|
||||
|
||||
override public func copyText(withTimestamp: Bool, withSender: Bool) -> String? {
|
||||
if let prefix = super.copyText(withTimestamp: withTimestamp, withSender: withSender) {
|
||||
return "\(prefix) \(url)";
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatAttachmentAppendix: Codable {
|
||||
|
||||
var state: State = .new;
|
||||
var filesize: Int? = nil;
|
||||
var mimetype: String? = nil;
|
||||
var filename: String? = nil;
|
||||
|
||||
init() {}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self);
|
||||
state = State(rawValue: try container.decode(Int.self, forKey: .state))!;
|
||||
filesize = try container.decodeIfPresent(Int.self, forKey: .filesize);
|
||||
mimetype = try container.decodeIfPresent(String.self, forKey: .mimetype);
|
||||
filename = try container.decodeIfPresent(String.self, forKey: .filename);
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self);
|
||||
try container.encode(state.rawValue, forKey: .state);
|
||||
|
||||
if let filesize = self.filesize {
|
||||
try container.encode(filesize, forKey: .filesize);
|
||||
}
|
||||
if let mimetype = self.mimetype {
|
||||
try container.encode(mimetype, forKey: .mimetype);
|
||||
}
|
||||
if let filename = self.filename {
|
||||
try container.encode(filename, forKey: .filename);
|
||||
}
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case state = "state"
|
||||
case filesize = "size"
|
||||
case mimetype = "mimetype"
|
||||
case filename = "name"
|
||||
}
|
||||
|
||||
enum State: Int {
|
||||
case new
|
||||
case downloaded
|
||||
case removed
|
||||
case tooBig
|
||||
case error
|
||||
case gone
|
||||
}
|
||||
}
|
||||
|
102
SiskinIM/chats/ChatEntry.swift
Normal file
102
SiskinIM/chats/ChatEntry.swift
Normal file
|
@ -0,0 +1,102 @@
|
|||
//
|
||||
// ChatEntry.swift
|
||||
//
|
||||
// Siskin IM
|
||||
// Copyright (C) 2019 "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
|
||||
|
||||
public class ChatEntry: ChatViewItemProtocol {
|
||||
|
||||
static let timestampFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter();
|
||||
formatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "dd.MM.yyyy jj:mm", options: 0, locale: NSLocale.current);
|
||||
return formatter;
|
||||
}();
|
||||
|
||||
|
||||
public let id: Int;
|
||||
public let timestamp: Date;
|
||||
public let account: BareJID;
|
||||
public let jid: BareJID;
|
||||
public let state: MessageState;
|
||||
|
||||
// for MUC only but any chat may be a MUC chat...
|
||||
public let authorNickname: String?;
|
||||
public let authorJid: BareJID?;
|
||||
|
||||
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?, encryption: MessageEncryption, encryptionFingerprint: String?, error: String?) {
|
||||
self.id = id;
|
||||
self.timestamp = timestamp;
|
||||
self.account = account;
|
||||
self.jid = jid;
|
||||
self.state = state;
|
||||
self.authorNickname = authorNickname;
|
||||
self.authorJid = authorJid;
|
||||
self.encryption = encryption;
|
||||
self.encryptionFingerprint = encryptionFingerprint;
|
||||
self.error = error;
|
||||
}
|
||||
|
||||
public func isMergeable(with chatItem: ChatViewItemProtocol) -> Bool {
|
||||
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 && abs(self.timestamp.timeIntervalSince(item.timestamp)) < allowedTimeDiff() && self.encryption == item.encryption && self.encryptionFingerprint == item.encryptionFingerprint;
|
||||
}
|
||||
|
||||
public func copyText(withTimestamp: Bool, withSender: Bool) -> String? {
|
||||
if withSender {
|
||||
switch state.direction {
|
||||
case .incoming:
|
||||
if let nickname = authorNickname {
|
||||
return nickname;
|
||||
} else if let rosterModule: RosterModule = XmppService.instance.getClient(for: account)?.modulesManager.getModule(RosterModule.ID), let rosterItem = rosterModule.rosterStore.get(for: JID(jid)), let nickname = rosterItem.name {
|
||||
return nickname;
|
||||
} else {
|
||||
return jid.stringValue;
|
||||
}
|
||||
case .outgoing:
|
||||
return "Me:";
|
||||
}
|
||||
} else {
|
||||
if withTimestamp {
|
||||
return "[\(ChatEntry.timestampFormatter.string(from: timestamp))]";
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func allowedTimeDiff() -> TimeInterval {
|
||||
switch /*Settings.messageGrouping.string() ??*/ "smart" {
|
||||
case "none":
|
||||
return -1.0;
|
||||
case "always":
|
||||
return 60.0 * 60.0 * 24.0;
|
||||
default:
|
||||
return 30.0;
|
||||
}
|
||||
}
|
||||
}
|
37
SiskinIM/chats/ChatLinkPreview.swift
Normal file
37
SiskinIM/chats/ChatLinkPreview.swift
Normal file
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// ChatLinkPreview.swift
|
||||
//
|
||||
// Siskin IM
|
||||
// Copyright (C) 2019 "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 ChatLinkPreview: ChatEntry {
|
||||
|
||||
let url: String;
|
||||
|
||||
init(id: Int, timestamp: Date, account: BareJID, jid: BareJID, state: MessageState, url: String, authorNickname: String?, authorJid: BareJID?, 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, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: error);
|
||||
}
|
||||
|
||||
override func copyText(withTimestamp: Bool, withSender: Bool) -> String? {
|
||||
return nil;
|
||||
}
|
||||
}
|
42
SiskinIM/chats/ChatMessage.swift
Normal file
42
SiskinIM/chats/ChatMessage.swift
Normal file
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// ChatMessage.swift
|
||||
//
|
||||
// Siskin IM
|
||||
// Copyright (C) 2019 "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 ChatMessage: ChatEntry {
|
||||
|
||||
let message: String;
|
||||
|
||||
init(id: Int, timestamp: Date, account: BareJID, jid: BareJID, state: MessageState, message: String, authorNickname: String?, authorJid: BareJID?, 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, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: error);
|
||||
}
|
||||
|
||||
override func copyText(withTimestamp: Bool, withSender: Bool) -> String? {
|
||||
if let prefix = super.copyText(withTimestamp: withTimestamp, withSender: withSender) {
|
||||
return "\(prefix) \(message)";
|
||||
} else {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,186 +23,53 @@
|
|||
import UIKit
|
||||
import TigaseSwift
|
||||
|
||||
class ChatTableViewCell: UITableViewCell, UIDocumentInteractionControllerDelegate {
|
||||
class ChatTableViewCell: BaseChatTableViewCell, UIContextMenuInteractionDelegate {
|
||||
|
||||
@IBOutlet var avatarView: AvatarView?
|
||||
@IBOutlet var nicknameView: UILabel?;
|
||||
@IBOutlet var messageTextView: UILabel!
|
||||
@IBOutlet var messageFrameView: UIView?
|
||||
@IBOutlet var timestampView: UILabel?
|
||||
@IBOutlet var stateView: UILabel?;
|
||||
|
||||
@IBOutlet var previewView: UIImageView?;
|
||||
|
||||
fileprivate var previewUrl: URL?;
|
||||
|
||||
|
||||
fileprivate var messageLinkTapGestureRecognizer: UITapGestureRecognizer!;
|
||||
fileprivate var previewViewTapGestureRecognizer: UITapGestureRecognizer?;
|
||||
|
||||
fileprivate var originalTextColor: UIColor!;
|
||||
fileprivate var links: [Link] = [];
|
||||
|
||||
fileprivate static let todaysFormatter = ({()-> DateFormatter in
|
||||
var f = DateFormatter();
|
||||
f.dateStyle = .none;
|
||||
f.timeStyle = .short;
|
||||
return f;
|
||||
})();
|
||||
fileprivate static let defaultFormatter = ({()-> DateFormatter in
|
||||
var f = DateFormatter();
|
||||
f.dateFormat = DateFormatter.dateFormat(fromTemplate: "dd.MM, jj:mm", options: 0, locale: NSLocale.current);
|
||||
// f.timeStyle = .NoStyle;
|
||||
return f;
|
||||
})();
|
||||
fileprivate static let fullFormatter = ({()-> DateFormatter in
|
||||
var f = DateFormatter();
|
||||
f.dateFormat = DateFormatter.dateFormat(fromTemplate: "dd.MM.yyyy, jj:mm", options: 0, locale: NSLocale.current);
|
||||
// f.timeStyle = .NoStyle;
|
||||
return f;
|
||||
})();
|
||||
private var item: ChatMessage?;
|
||||
|
||||
fileprivate func formatTimestamp(_ ts: Date) -> String {
|
||||
let flags: Set<Calendar.Component> = [.day, .year];
|
||||
let components = Calendar.current.dateComponents(flags, from: ts, to: Date());
|
||||
if (components.day! < 1) {
|
||||
return ChatTableViewCell.todaysFormatter.string(from: ts);
|
||||
override var backgroundColor: UIColor? {
|
||||
didSet {
|
||||
self.messageTextView.backgroundColor = self.backgroundColor;
|
||||
}
|
||||
if (components.year! != 0) {
|
||||
return ChatTableViewCell.fullFormatter.string(from: ts);
|
||||
} else {
|
||||
return ChatTableViewCell.defaultFormatter.string(from: ts);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
// Initialization code
|
||||
if messageFrameView != nil {
|
||||
originalTextColor = messageTextView.textColor;
|
||||
//messageFrameView.backgroundColor = UIColor.li();
|
||||
messageFrameView?.layer.masksToBounds = true;
|
||||
messageFrameView?.layer.cornerRadius = 6;
|
||||
} else {
|
||||
originalTextColor = messageTextView.textColor;
|
||||
if previewView != nil {
|
||||
previewView?.layer.masksToBounds = true;
|
||||
previewView?.layer.cornerRadius = 6;
|
||||
}
|
||||
}
|
||||
if avatarView != nil {
|
||||
avatarView!.layer.masksToBounds = true;
|
||||
avatarView!.layer.cornerRadius = avatarView!.frame.height / 2;
|
||||
}
|
||||
originalTextColor = messageTextView.textColor;
|
||||
messageLinkTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(messageLinkTapGestureDidFire));
|
||||
messageLinkTapGestureRecognizer.numberOfTapsRequired = 1;
|
||||
messageLinkTapGestureRecognizer.cancelsTouchesInView = false;
|
||||
messageTextView.addGestureRecognizer(messageLinkTapGestureRecognizer);
|
||||
if previewView != nil {
|
||||
previewViewTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(previewTapGestureDidFire));
|
||||
messageLinkTapGestureRecognizer.cancelsTouchesInView = false;
|
||||
previewViewTapGestureRecognizer?.numberOfTapsRequired = 2;
|
||||
previewView?.addGestureRecognizer(previewViewTapGestureRecognizer!);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
if selected {
|
||||
let colors = contentView.subviews.map({ it -> UIColor in it.backgroundColor ?? UIColor.clear });
|
||||
super.setSelected(selected, animated: animated)
|
||||
selectedBackgroundView = UIView();
|
||||
contentView.subviews.enumerated().forEach { (offset, view) in
|
||||
if view .responds(to: #selector(setHighlighted(_:animated:))) {
|
||||
view.setValue(false, forKey: "highlighted");
|
||||
}
|
||||
print("offset", offset, "view", view);
|
||||
view.backgroundColor = colors[offset];
|
||||
}
|
||||
} else {
|
||||
super.setSelected(selected, animated: animated);
|
||||
selectedBackgroundView = nil;
|
||||
}
|
||||
// Configure the view for the selected state
|
||||
}
|
||||
|
||||
|
||||
func set(message item: ChatMessage, downloader: ((URL,Int,BareJID,BareJID)->Void)? = nil) {
|
||||
var timestamp = formatTimestamp(item.timestamp);
|
||||
switch item.encryption {
|
||||
case .decrypted, .notForThisDevice, .decryptionFailed:
|
||||
timestamp = "\u{1F512} \(timestamp)";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch item.state {
|
||||
case .incoming_error, .incoming_error_unread:
|
||||
//elf.message.textColor = NSColor.systemRed;
|
||||
self.stateView?.text = "\u{203c}";
|
||||
case .outgoing_unsent:
|
||||
//self.message.textColor = NSColor.secondaryLabelColor;
|
||||
self.stateView?.text = "\u{1f4e4}";
|
||||
case .outgoing_delivered:
|
||||
//self.message.textColor = nil;
|
||||
self.stateView?.text = "\u{2713}";
|
||||
case .outgoing_error, .outgoing_error_unread:
|
||||
//self.message.textColor = nil;
|
||||
self.stateView?.text = "\u{203c}";
|
||||
default:
|
||||
//self.state?.stringValue = "";
|
||||
self.stateView?.text = nil;//NSColor.textColor;
|
||||
}
|
||||
if stateView == nil {
|
||||
if item.state.direction == .outgoing {
|
||||
timestampView?.textColor = UIColor.lightGray;
|
||||
if item.state.isError {
|
||||
timestampView?.textColor = UIColor.red;
|
||||
timestamp = "\(timestamp) Not delivered\u{203c}";
|
||||
} else if item.state == .outgoing_delivered {
|
||||
timestamp = "\(timestamp) \u{2713}";
|
||||
}
|
||||
}
|
||||
}
|
||||
timestampView?.text = timestamp;
|
||||
self.previewUrl = nil;
|
||||
self.previewView?.image = nil;
|
||||
|
||||
if messageFrameView != nil {
|
||||
self.messageFrameView?.backgroundColor = item.state.direction == .incoming ? Appearance.current.incomingBubbleColor() : Appearance.current.outgoingBubbleColor();
|
||||
self.nicknameView?.textColor = Appearance.current.secondaryLabelColor;
|
||||
self.messageTextView.textColor = self.originalTextColor;
|
||||
} else {
|
||||
self.nicknameView?.textColor = Appearance.current.labelColor;
|
||||
self.messageTextView?.textColor = Appearance.current.secondaryLabelColor;
|
||||
if #available(iOS 13.0, *) {
|
||||
messageTextView.addInteraction(UIContextMenuInteraction(delegate: self));
|
||||
}
|
||||
}
|
||||
|
||||
func set(message item: ChatMessage) {
|
||||
self.item = item;
|
||||
super.set(item: item);
|
||||
|
||||
self.messageTextView?.textColor = Appearance.current.secondaryLabelColor;
|
||||
|
||||
self.links.removeAll();
|
||||
|
||||
var previewRange: NSRange? = nil;
|
||||
var previewSourceUrl: URL? = nil;
|
||||
let attrText = NSMutableAttributedString(string: item.message);
|
||||
|
||||
var first = true;
|
||||
if let detect = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue | NSTextCheckingResult.CheckingType.phoneNumber.rawValue | NSTextCheckingResult.CheckingType.address.rawValue | NSTextCheckingResult.CheckingType.date.rawValue) {
|
||||
let matches = detect.matches(in: item.message, options: .reportCompletion, range: NSMakeRange(0, item.message.count));
|
||||
for match in matches {
|
||||
var url: URL? = nil;
|
||||
if match.url != nil {
|
||||
url = match.url;
|
||||
if first {
|
||||
first = false;
|
||||
if (item.preview?.hasPrefix("preview:image:") ?? true) {
|
||||
let previewKey = item.preview == nil ? nil : String(item.preview!.dropFirst(14));
|
||||
previewView?.image = ImageCache.shared.get(for: previewKey, ifMissing: {
|
||||
downloader?(url!, item.id, item.account, item.jid);
|
||||
})
|
||||
if previewView?.image != nil && previewKey != nil {
|
||||
previewUrl = ImageCache.shared.getURL(for: previewKey);
|
||||
previewRange = match.range;
|
||||
previewSourceUrl = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if match.phoneNumber != nil {
|
||||
url = URL(string: "tel:\(match.phoneNumber!.replacingOccurrences(of: " ", with: "-"))");
|
||||
|
@ -217,13 +84,10 @@ class ChatTableViewCell: UITableViewCell, UIDocumentInteractionControllerDelegat
|
|||
}
|
||||
if url != nil {
|
||||
self.links.append(Link(url: url!, range: match.range));
|
||||
attrText.setAttributes([NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue, NSAttributedString.Key.foregroundColor: (Appearance.current.isDark && Settings.EnableNewUI.getBool()) ? UIColor.blue.adjust(brightness: 0.75) : UIColor.blue], range: match.range);
|
||||
attrText.setAttributes([NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue, NSAttributedString.Key.foregroundColor: (Appearance.current.isDark) ? UIColor.blue.adjust(brightness: 0.75) : UIColor.blue], range: match.range);
|
||||
}
|
||||
}
|
||||
}
|
||||
if previewSourceUrl != nil && Settings.SimplifiedLinkToFileIfPreviewIsAvailable.getBool() {
|
||||
attrText.mutableString.replaceCharacters(in: previewRange!, with: "Link to file");
|
||||
}
|
||||
if Settings.EnableMarkdownFormatting.getBool() {
|
||||
Markdown.applyStyling(attributedString: attrText, font: self.messageTextView.font, showEmoticons:Settings.ShowEmoticons.getBool());
|
||||
}
|
||||
|
@ -234,37 +98,46 @@ class ChatTableViewCell: UITableViewCell, UIDocumentInteractionControllerDelegat
|
|||
}
|
||||
if item.state.direction == .incoming {
|
||||
self.messageTextView.textColor = UIColor.red;
|
||||
} else {
|
||||
self.accessoryType = .detailButton;
|
||||
self.tintColor = UIColor.red;
|
||||
}
|
||||
} else {
|
||||
self.accessoryType = .none;
|
||||
self.tintColor = self.messageTextView.tintColor;
|
||||
if item.encryption == .notForThisDevice || item.encryption == .decryptionFailed {
|
||||
if let messageFrameView = self.messageFrameView {
|
||||
self.messageTextView.textColor = self.originalTextColor.mix(color: messageFrameView.backgroundColor!, ratio: 0.33);
|
||||
} else {
|
||||
self.messageTextView.textColor = Appearance.current.labelColor;
|
||||
}
|
||||
self.messageTextView.textColor = Appearance.current.labelColor;
|
||||
}
|
||||
}
|
||||
self.stateView?.textColor = self.messageTextView.textColor;
|
||||
if messageFrameView != nil {
|
||||
self.timestampView?.textColor = Appearance.current.labelColor;
|
||||
} else {
|
||||
self.timestampView?.textColor = self.messageTextView.textColor;
|
||||
}
|
||||
}
|
||||
|
||||
@objc func actionMore(_ sender: UIMenuController) {
|
||||
NotificationCenter.default.post(name: NSNotification.Name("tableViewCellShowEditToolbar"), object: self);
|
||||
@available(iOS 13.0, *)
|
||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||
var cfg = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { suggestedActions -> UIMenu? in
|
||||
return self.prepareContextMenu();
|
||||
};
|
||||
return cfg;
|
||||
}
|
||||
|
||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
return super.canPerformAction(action, withSender: sender) || action == #selector(actionMore(_:));
|
||||
@available(iOS 13.0, *)
|
||||
func prepareContextMenu() -> UIMenu {
|
||||
let items = [
|
||||
UIAction(title: "Copy", image: UIImage(systemName: "doc.on.doc"), handler: { action in
|
||||
guard let text = self.item?.copyText(withTimestamp: Settings.CopyMessagesWithTimestamps.getBool(), withSender: false) else {
|
||||
return;
|
||||
}
|
||||
UIPasteboard.general.strings = [text];
|
||||
UIPasteboard.general.string = text;
|
||||
}),
|
||||
UIAction(title: "Share..", image: UIImage(systemName: "square.and.arrow.up"), handler: { action in
|
||||
guard let text = self.item?.copyText(withTimestamp: Settings.CopyMessagesWithTimestamps.getBool(), withSender: false) else {
|
||||
return;
|
||||
}
|
||||
let activityController = UIActivityViewController(activityItems: [text], applicationActivities: nil);
|
||||
(UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController?.present(activityController, animated: true, completion: nil);
|
||||
}),
|
||||
UIAction(title: "More..", image: UIImage(systemName: "ellipsis"), handler: { action in
|
||||
NotificationCenter.default.post(name: Notification.Name("tableViewCellShowEditToolbar"), object: self);
|
||||
})
|
||||
];
|
||||
return UIMenu(title: "", children: items);
|
||||
}
|
||||
|
||||
|
||||
@objc func messageLinkTapGestureDidFire(_ recognizer: UITapGestureRecognizer) {
|
||||
guard self.messageTextView.attributedText != nil else {
|
||||
return;
|
||||
|
@ -289,18 +162,7 @@ class ChatTableViewCell: UITableViewCell, UIDocumentInteractionControllerDelegat
|
|||
UIApplication.shared.open(url.url);
|
||||
}
|
||||
}
|
||||
|
||||
@objc func previewTapGestureDidFire(_ recognizer: UITapGestureRecognizer) {
|
||||
guard self.previewView != nil else {
|
||||
return;
|
||||
}
|
||||
|
||||
let documentController = UIDocumentInteractionController(url: previewUrl!);
|
||||
documentController.delegate = self;
|
||||
//documentController.presentPreview(animated: true);
|
||||
documentController.presentOptionsMenu(from: CGRect.zero, in: self.previewView!, animated: true);
|
||||
}
|
||||
|
||||
class Link {
|
||||
let url: URL;
|
||||
let range: NSRange;
|
||||
|
|
|
@ -114,7 +114,7 @@ class ChatViewController : BaseChatViewControllerWithDataSourceAndContextMenuAnd
|
|||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
NotificationCenter.default.removeObserver(self);
|
||||
//NotificationCenter.default.removeObserver(self);
|
||||
super.viewDidDisappear(animated);
|
||||
}
|
||||
|
||||
|
@ -131,14 +131,17 @@ class ChatViewController : BaseChatViewControllerWithDataSourceAndContextMenuAnd
|
|||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
if dataSource.count == 0 {
|
||||
let label = UILabel(frame: CGRect(x: 0, y:0, width: self.view.bounds.size.width, height: self.view.bounds.size.height));
|
||||
label.text = "No messages available. Pull up to refresh message history.";
|
||||
label.numberOfLines = 0;
|
||||
label.textAlignment = .center;
|
||||
label.transform = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0);
|
||||
label.sizeToFit();
|
||||
label.textColor = Appearance.current.labelColor;
|
||||
self.tableView.backgroundView = label;
|
||||
if self.tableView.backgroundView == nil {
|
||||
let label = UILabel(frame: CGRect(x: 0, y:0, width: self.view.bounds.size.width, height: self.view.bounds.size.height));
|
||||
label.text = "No messages available. Pull up to refresh message history.";
|
||||
label.font = UIFont.systemFont(ofSize: UIFont.systemFontSize + 2, weight: .medium);
|
||||
label.numberOfLines = 0;
|
||||
label.textAlignment = .center;
|
||||
label.transform = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0);
|
||||
label.sizeToFit();
|
||||
label.textColor = Appearance.current.secondaryLabelColor;
|
||||
self.tableView.backgroundView = label;
|
||||
}
|
||||
} else {
|
||||
self.tableView.backgroundView = nil;
|
||||
}
|
||||
|
@ -150,25 +153,44 @@ class ChatViewController : BaseChatViewControllerWithDataSourceAndContextMenuAnd
|
|||
return tableView.dequeueReusableCell(withIdentifier: "ChatTableViewCellIncoming", for: indexPath);
|
||||
}
|
||||
|
||||
var continuation = false;
|
||||
if (indexPath.row + 1) < dataSource.count {
|
||||
if let prevItem = dataSource.getItem(at: indexPath.row + 1) {
|
||||
continuation = dsItem.isMergeable(with: prevItem);
|
||||
}
|
||||
}
|
||||
let incoming = dsItem.state.direction == .incoming;
|
||||
|
||||
switch dsItem {
|
||||
case let item as ChatMessage:
|
||||
var continuation = false;
|
||||
if Settings.EnableNewUI.getBool() && (indexPath.row + 1) < dataSource.count {
|
||||
if let prevItem = dataSource.getItem(at: indexPath.row + 1) {
|
||||
continuation = item.isMergeable(with: prevItem);
|
||||
}
|
||||
}
|
||||
let incoming = item.state.direction == .incoming;
|
||||
let id = Settings.EnableNewUI.getBool() ? (continuation ? "ChatTableViewCellContinuation" : "ChatTableViewCell") : (incoming ? "ChatTableViewCellIncoming" : "ChatTableViewCellOutgoing");
|
||||
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;
|
||||
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;
|
||||
cell.set(message: item, downloader: self.downloadPreview(url:msgId:account:jid:));
|
||||
cell.setNeedsUpdateConstraints();
|
||||
cell.updateConstraintsIfNeeded();
|
||||
cell.set(message: item);
|
||||
// cell.setNeedsUpdateConstraints();
|
||||
// cell.updateConstraintsIfNeeded();
|
||||
|
||||
return cell;
|
||||
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;
|
||||
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;
|
||||
cell.set(attachment: item);
|
||||
// cell.setNeedsUpdateConstraints();
|
||||
// cell.updateConstraintsIfNeeded();
|
||||
|
||||
return cell;
|
||||
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.set(linkPreview: item);
|
||||
return cell;
|
||||
case let item as SystemMessage:
|
||||
let cell: ChatTableViewSystemCell = tableView.dequeueReusableCell(withIdentifier: "ChatTableViewSystemCell", for: indexPath) as! ChatTableViewSystemCell;
|
||||
|
@ -182,17 +204,27 @@ class ChatViewController : BaseChatViewControllerWithDataSourceAndContextMenuAnd
|
|||
|
||||
func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
|
||||
print("accessory button cliecked at", indexPath)
|
||||
guard let item = dataSource.getItem(at: indexPath.row) as? ChatMessage else {
|
||||
guard let item = dataSource.getItem(at: indexPath.row) as? ChatEntry, let chat = self.chat as? DBChat else {
|
||||
return;
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let alert = UIAlertController(title: "Details", message: item.error ?? "Unknown error occurred", preferredStyle: .alert);
|
||||
alert.addAction(UIAlertAction(title: "Resend", style: .default, handler: {(action) in
|
||||
print("resending message with body", item.message);
|
||||
let url = item.message.starts(with: "http:") || item.message.starts(with: "https:") ? item.message : nil;
|
||||
self.sendMessage(body: item.message, url: url, completed: nil);
|
||||
DBChatHistoryStore.instance.removeItem(for: item.account, with: item.jid, itemId: item.id);
|
||||
//print("resending message with body", item.message);
|
||||
|
||||
switch item {
|
||||
case let item as ChatMessage:
|
||||
MessageEventHandler.sendMessage(chat: chat, body: item.message, url: nil);
|
||||
DBChatHistoryStore.instance.removeItem(for: chat.account, with: chat.jid.bareJid, itemId: item.id);
|
||||
case let item as ChatAttachment:
|
||||
let oldLocalFile = DownloadStore.instance.url(for: "\(item.id)");
|
||||
MessageEventHandler.sendAttachment(chat: chat, originalUrl: oldLocalFile, uploadedUrl: item.url, appendix: item.appendix, completionHandler: {
|
||||
DBChatHistoryStore.instance.removeItem(for:chat.account, with: chat.jid.bareJid, itemId: item.id);
|
||||
});
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}));
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil));
|
||||
self.present(alert, animated: true, completion: nil);
|
||||
|
@ -365,16 +397,19 @@ class ChatViewController : BaseChatViewControllerWithDataSourceAndContextMenuAnd
|
|||
return;
|
||||
}
|
||||
|
||||
sendMessage(body: text!, completed: {() in
|
||||
DispatchQueue.main.async {
|
||||
self.messageText = nil;
|
||||
}
|
||||
});
|
||||
MessageEventHandler.sendMessage(chat: self.chat as! DBChat, body: text, url: nil);
|
||||
DispatchQueue.main.async {
|
||||
self.messageText = nil;
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(body: String, url: String? = nil, preview: String? = nil, completed: (()->Void)?) {
|
||||
MessageEventHandler.sendMessage(chat: self.chat as! DBChat, body: body, url: url);
|
||||
completed?();
|
||||
|
||||
func sendAttachment(originalUrl: URL?, uploadedUrl: String, appendix: ChatAttachmentAppendix, completionHandler: (() -> Void)?) {
|
||||
guard let chat = self.chat as? DBChat else {
|
||||
completionHandler?();
|
||||
return;
|
||||
}
|
||||
MessageEventHandler.sendAttachment(chat: chat, originalUrl: originalUrl, uploadedUrl: uploadedUrl, appendix: appendix, completionHandler: completionHandler);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -427,7 +427,11 @@ class ChatViewDataSource {
|
|||
let unsent1 = it1.state == .outgoing_unsent;
|
||||
let unsent2 = it2.state == .outgoing_unsent;
|
||||
if unsent1 == unsent2 {
|
||||
return it1.timestamp.compare(it2.timestamp);
|
||||
let result = it1.timestamp.compare(it2.timestamp);
|
||||
guard result == .orderedSame else {
|
||||
return result;
|
||||
}
|
||||
return it1.id < it2.id ? .orderedAscending : .orderedDescending;
|
||||
} else {
|
||||
if unsent1 {
|
||||
return .orderedDescending;
|
||||
|
@ -482,6 +486,10 @@ class SystemMessage: ChatViewItemProtocol {
|
|||
func isMergeable(with item: ChatViewItemProtocol) -> Bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
func copyText(withTimestamp: Bool, withSender: Bool) -> String? {
|
||||
return nil;
|
||||
}
|
||||
|
||||
enum Kind {
|
||||
case unreadMessages
|
||||
|
|
|
@ -32,4 +32,5 @@ public protocol ChatViewItemProtocol: class {
|
|||
var encryptionFingerprint: String? { get };
|
||||
|
||||
func isMergeable(with item: ChatViewItemProtocol) -> Bool;
|
||||
func copyText(withTimestamp: Bool, withSender: Bool) -> String?;
|
||||
}
|
||||
|
|
|
@ -80,17 +80,13 @@ class ChatsListViewController: CustomTableViewController {
|
|||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cellIdentifier = Settings.EnableNewUI.getBool() ? "ChatsListTableViewCellNew" : "ChatsListTableViewCell";
|
||||
let cellIdentifier = "ChatsListTableViewCellNew";
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath as IndexPath) as! ChatsListTableViewCell;
|
||||
|
||||
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);
|
||||
// if Settings.EnableNewUI.getBool() {
|
||||
cell.lastMessageLabel.textColor = Appearance.current.labelColor;
|
||||
// } else {
|
||||
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())
|
||||
|
|
82
SiskinIM/chats/LinkPreviewChatTableViewCell.swift
Normal file
82
SiskinIM/chats/LinkPreviewChatTableViewCell.swift
Normal file
|
@ -0,0 +1,82 @@
|
|||
//
|
||||
// LinkPreviewChatTableViewCell.swift
|
||||
//
|
||||
// Siskin IM
|
||||
// Copyright (C) 2019 "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 UIKit
|
||||
import LinkPresentation
|
||||
|
||||
class LinkPreviewChatTableViewCell: BaseChatTableViewCell {
|
||||
|
||||
var linkView: UIView? {
|
||||
didSet {
|
||||
if let value = oldValue {
|
||||
if #available(iOS 13.0, *) {
|
||||
(value as! LPLinkView).metadata = LPLinkMetadata();
|
||||
}
|
||||
value.removeFromSuperview();
|
||||
}
|
||||
if let value = linkView {
|
||||
self.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)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func set(linkPreview item: ChatLinkPreview) {
|
||||
super.set(item: item);
|
||||
if #available(iOS 13.0, *) {
|
||||
var metadata = MetadataCache.instance.metadata(for: "\(item.id)");
|
||||
var isNew = false;
|
||||
let url = URL(string: item.url)!;
|
||||
|
||||
if (metadata == nil) {
|
||||
metadata = LPLinkMetadata();
|
||||
metadata!.originalURL = url;
|
||||
isNew = true;
|
||||
}
|
||||
//if self.linkView == nil {
|
||||
self.linkView = LPLinkView(url: url);
|
||||
linkView?.setContentCompressionResistancePriority(.defaultHigh, for: .vertical);
|
||||
linkView?.setContentCompressionResistancePriority(.defaultLow, for: .horizontal);
|
||||
linkView?.translatesAutoresizingMaskIntoConstraints = false;
|
||||
//};
|
||||
|
||||
let linkView = self.linkView as! LPLinkView;
|
||||
linkView.metadata = metadata!;
|
||||
linkView.overrideUserInterfaceStyle = Appearance.current.isDark ? .dark : .light;
|
||||
|
||||
|
||||
if isNew {
|
||||
MetadataCache.instance.generateMetadata(for: url, withId: "\(item.id)", completionHandler: { [weak linkView] meta1 in
|
||||
guard let meta = meta1 else {
|
||||
return;
|
||||
}
|
||||
DBChatHistoryStore.instance.itemUpdated(withId: item.id, for: item.account, with: item.jid);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -97,6 +97,7 @@ class MucChatSettingsViewController: CustomTableViewController, UIImagePickerCon
|
|||
})
|
||||
dispatchGroup.enter();
|
||||
vcardTempModule.retrieveVCard(from: room.jid, onSuccess: { (vcard) in
|
||||
XmppService.instance.dbVCardsCache.updateVCard(for: self.room.roomJid, on: self.account, vcard: vcard);
|
||||
DispatchQueue.main.async {
|
||||
self.canEditVCard = true;
|
||||
dispatchGroup.leave();
|
||||
|
@ -185,6 +186,15 @@ class MucChatSettingsViewController: CustomTableViewController, UIImagePickerCon
|
|||
}
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
if segue.identifier == "chatShowAttachments" {
|
||||
if let attachmentsController = segue.destination as? ChatAttachmentsController {
|
||||
attachmentsController.account = self.account;
|
||||
attachmentsController.jid = self.room.roomJid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func editClicked(_ sender: UIBarButtonItem) {
|
||||
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet);
|
||||
alertController.addAction(UIAlertAction(title: "Rename chat", style: .default, handler: { (action) in
|
||||
|
|
|
@ -107,16 +107,16 @@ class MucChatViewController: BaseChatViewControllerWithDataSourceAndContextMenuA
|
|||
return tableView.dequeueReusableCell(withIdentifier: "MucChatTableViewCellIncoming", for: indexPath);
|
||||
}
|
||||
|
||||
var continuation = false;
|
||||
if (indexPath.row + 1) < dataSource.count {
|
||||
if let prevItem = dataSource.getItem(at: indexPath.row + 1) {
|
||||
continuation = dbItem.isMergeable(with: prevItem);
|
||||
}
|
||||
}
|
||||
|
||||
switch dbItem {
|
||||
case let item as ChatMessage:
|
||||
var continuation = false;
|
||||
if Settings.EnableNewUI.getBool() && (indexPath.row + 1) < dataSource.count {
|
||||
if let prevItem = dataSource.getItem(at: indexPath.row + 1) {
|
||||
continuation = item.isMergeable(with: prevItem);
|
||||
}
|
||||
}
|
||||
|
||||
let id = Settings.EnableNewUI.getBool() ? (continuation ? "MucChatTableViewCellContinuation" : "MucChatTableViewCell") : (item.state.direction == .incoming ? "MucChatTableViewCellIncoming" : "MucChatTableViewCellOutgoing")
|
||||
let id = continuation ? "MucChatTableViewMessageContinuationCell" : "MucChatTableViewMessageCell";
|
||||
|
||||
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;
|
||||
|
@ -131,9 +131,34 @@ class MucChatViewController: BaseChatViewControllerWithDataSourceAndContextMenuA
|
|||
}
|
||||
}
|
||||
cell.nicknameView?.text = item.authorNickname;
|
||||
cell.set(message: item, downloader: downloadPreview(url:msgId:account:jid:));
|
||||
cell.set(message: item);
|
||||
cell.backgroundColor = Appearance.current.systemBackground;
|
||||
return cell;
|
||||
case let item as ChatAttachment:
|
||||
let id = continuation ? "MucChatTableViewAttachmentContinuationCell" : "MucChatTableViewAttachmentCell";
|
||||
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;
|
||||
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);
|
||||
} else if let nickname = item.authorNickname, let photoHash = self.room?.presences[nickname]?.presence.vcardTempPhoto {
|
||||
cell.avatarView?.set(name: item.authorNickname, avatar: AvatarManager.instance.avatar(withHash: photoHash), orDefault: AvatarManager.instance.defaultAvatar);
|
||||
} else {
|
||||
cell.avatarView?.set(name: item.authorNickname, avatar: nil, orDefault: AvatarManager.instance.defaultAvatar);
|
||||
}
|
||||
}
|
||||
cell.nicknameView?.text = item.authorNickname;
|
||||
cell.set(attachment: item);
|
||||
cell.setNeedsUpdateConstraints();
|
||||
cell.updateConstraintsIfNeeded();
|
||||
|
||||
return cell;
|
||||
case let item as ChatLinkPreview:
|
||||
let id = "MucChatTableViewLinkPreviewCell";
|
||||
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.set(linkPreview: item);
|
||||
return cell;
|
||||
case let item as SystemMessage:
|
||||
let cell: ChatTableViewSystemCell = tableView.dequeueReusableCell(withIdentifier: "MucChatTableViewSystemCell", for: indexPath) as! ChatTableViewSystemCell;
|
||||
cell.set(item: item);
|
||||
|
@ -223,22 +248,21 @@ class MucChatViewController: BaseChatViewControllerWithDataSourceAndContextMenuA
|
|||
return;
|
||||
}
|
||||
|
||||
self.sendMessage(body: text!, url: nil, completed: {() in
|
||||
DispatchQueue.main.async {
|
||||
self.messageText = nil;
|
||||
}
|
||||
});
|
||||
self.room!.sendMessage(text, url: nil, additionalElements: []);
|
||||
DispatchQueue.main.async {
|
||||
self.messageText = nil;
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func shareClicked(_ sender: UIButton) {
|
||||
self.showPhotoSelector(sender);
|
||||
}
|
||||
|
||||
func sendMessage(body: String, url: String? = nil, preview: String? = nil, completed: (()->Void)?) {
|
||||
self.room!.sendMessage(body, url: url, additionalElements: []);
|
||||
completed?();
|
||||
func sendAttachment(originalUrl: URL?, uploadedUrl: String, appendix: ChatAttachmentAppendix, completionHandler: (() -> Void)?) {
|
||||
self.room!.sendMessage(uploadedUrl, url: uploadedUrl, additionalElements: []);
|
||||
completionHandler?();
|
||||
}
|
||||
|
||||
|
||||
@objc func roomInfoClicked() {
|
||||
print("room info for", account, room?.roomJid, "clicked!");
|
||||
guard let settingsController = self.storyboard?.instantiateViewController(withIdentifier: "MucChatSettingsViewController") as? MucChatSettingsViewController else {
|
||||
|
|
|
@ -104,6 +104,7 @@ class ContactViewController: CustomTableViewController {
|
|||
var sections: [Sections] = [.basic];
|
||||
if chat != nil {
|
||||
sections.append(.settings);
|
||||
sections.append(.attachments);
|
||||
sections.append(.encryption);
|
||||
}
|
||||
if phones.count > 0 {
|
||||
|
@ -131,6 +132,8 @@ class ContactViewController: CustomTableViewController {
|
|||
return 1;
|
||||
case .settings:
|
||||
return 2;
|
||||
case .attachments:
|
||||
return 1;
|
||||
case .encryption:
|
||||
return omemoIdentities.count + 1;
|
||||
case .phones:
|
||||
|
@ -191,6 +194,9 @@ class ContactViewController: CustomTableViewController {
|
|||
cell.accessoryView = btn;
|
||||
return cell;
|
||||
}
|
||||
case .attachments:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "AttachmentsCell", for: indexPath);
|
||||
return cell;
|
||||
case .encryption:
|
||||
if indexPath.row == 0 {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "OMEMOEncryptionCell", for: indexPath);
|
||||
|
@ -311,6 +317,8 @@ class ContactViewController: CustomTableViewController {
|
|||
return;
|
||||
case .settings:
|
||||
return;
|
||||
case .attachments:
|
||||
return;
|
||||
case .encryption:
|
||||
if indexPath.row == 0 {
|
||||
// handle change of encryption method!
|
||||
|
@ -365,6 +373,15 @@ class ContactViewController: CustomTableViewController {
|
|||
}
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
if segue.identifier == "chatShowAttachments" {
|
||||
if let attachmentsController = segue.destination as? ChatAttachmentsController {
|
||||
attachmentsController.account = self.account;
|
||||
attachmentsController.jid = self.jid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getVCardEntryTypeLabel(for type: VCard.EntryType) -> String? {
|
||||
switch type {
|
||||
case .home:
|
||||
|
@ -416,7 +433,6 @@ class ContactViewController: CustomTableViewController {
|
|||
break;
|
||||
case .failure(let err):
|
||||
AccountSettings.pushHash(account).set(int: 0);
|
||||
Dis
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -459,6 +475,7 @@ class ContactViewController: CustomTableViewController {
|
|||
enum Sections {
|
||||
case basic
|
||||
case settings
|
||||
case attachments
|
||||
case encryption
|
||||
case phones
|
||||
case emails
|
||||
|
@ -470,6 +487,8 @@ class ContactViewController: CustomTableViewController {
|
|||
return "";
|
||||
case .settings:
|
||||
return "Settings";
|
||||
case .attachments:
|
||||
return "";
|
||||
case .encryption:
|
||||
return "Encryption";
|
||||
case .phones:
|
||||
|
|
|
@ -32,11 +32,9 @@ open class DBChatHistoryStore: Logger {
|
|||
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, timestamp, item_type, data, stanza_id, state, encryption, fingerprint) VALUES (:account, :jid, :author_jid, :author_nickname, :timestamp, :item_type, :data, :stanza_id, :state, :encryption, :fingerprint)";
|
||||
fileprivate static let CHAT_MSG_APPEND = "INSERT INTO chat_history (account, jid, author_jid, author_nickname, timestamp, item_type, data, stanza_id, state, encryption, fingerprint, appendix) VALUES (:account, :jid, :author_jid, :author_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_GET = "SELECT id, author_jid, author_nickname, timestamp, item_type, data, state, preview, encryption, fingerprint FROM chat_history WHERE account = :account AND jid = :jid ORDER BY timestamp DESC LIMIT :limit OFFSET :offset"
|
||||
fileprivate static let CHAT_MSG_UPDATE_PREVIEW = "UPDATE chat_history SET preview = :preview WHERE id = :id";
|
||||
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)";
|
||||
|
@ -50,17 +48,18 @@ open class DBChatHistoryStore: Logger {
|
|||
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 msgUpdatePreviewStmt: DBStatement! = try? self.dbConnection.prepareStatement(DBChatHistoryStore.CHAT_MSG_UPDATE_PREVIEW);
|
||||
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 getMessagePositionStmt: DBStatement! = try? self.dbConnection.prepareStatement("SELECT count(id) FROM chat_history WHERE account = :account AND jid = :jid AND id <> :msgId AND timestamp < (SELECT timestamp FROM chat_history WHERE id = :msgId)");
|
||||
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 timestamp > (SELECT timestamp FROM chat_history WHERE id = :msgId)");
|
||||
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, author_nickname, author_jid, timestamp, item_type, data, state, preview, encryption, fingerprint, error FROM chat_history WHERE id = :id");
|
||||
fileprivate lazy var getChatMessageWithIdStmt: DBStatement! = try! self.dbConnection.prepareStatement("SELECT id, account, jid, author_nickname, author_jid, 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;
|
||||
|
||||
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)");
|
||||
|
||||
|
@ -74,33 +73,55 @@ open class DBChatHistoryStore: Logger {
|
|||
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.main.prepareStatement("SELECT id, author_nickname, author_jid, timestamp, item_type, data, state, preview, encryption, fingerprint, error FROM chat_history WHERE account = :account AND jid = :jid ORDER BY timestamp DESC LIMIT :limit OFFSET :offset");
|
||||
self.getChatMessagesStmt = try! dbConnection.prepareStatement("SELECT id, author_nickname, author_jid, 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, 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);
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(DBChatHistoryStore.imageRemovedFromCache), name: ImageCache.DISK_CACHE_IMAGE_REMOVED, object: nil);
|
||||
}
|
||||
|
||||
public func appendItem(for account: BareJID, with jid: BareJID, state inState: MessageState, authorNickname: String? = nil, authorJid: BareJID? = nil, type: ItemType = .message, timestamp: Date, stanzaId id: String?, data: String, chatState: ChatState? = nil, errorCondition: ErrorCondition? = nil, errorMessage: String? = nil, encryption: MessageEncryption, encryptionFingerprint: String?, completionHandler: ((Int)->Void)?) {
|
||||
public func appendItem(for account: BareJID, with jid: BareJID, state inState: MessageState, authorNickname: String? = nil, authorJid: BareJID? = 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)?) {
|
||||
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 {
|
||||
return;
|
||||
}
|
||||
|
||||
guard !self.checkItemAlreadyAdded(for: account, with: jid, authorNickname: authorNickname, type: type, timestamp: timestamp, direction: inState.direction, stanzaId: id, data: data) else {
|
||||
guard skipItemAlreadyExists || !self.checkItemAlreadyAdded(for: account, with: jid, authorNickname: authorNickname, type: type, timestamp: timestamp, direction: inState.direction, stanzaId: id, data: data) else {
|
||||
return;
|
||||
}
|
||||
|
||||
let state = self.calculateState(for: account, with: jid, timestamp: timestamp, state: inState);
|
||||
|
||||
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, "encryption": encryption.rawValue, "fingerprint": encryptionFingerprint]
|
||||
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, "encryption": encryption.rawValue, "fingerprint": encryptionFingerprint, "appendix": appendix]
|
||||
guard let msgId = try! self.appendMessageStmt.insert(params) else {
|
||||
return;
|
||||
}
|
||||
completionHandler?(msgId);
|
||||
|
||||
let item = ChatMessage(id: msgId, timestamp: timestamp, account: account, jid: jid, state: state, message: data, authorNickname: authorNickname, authorJid: authorJid, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: errorMessage);
|
||||
DBChatStore.instance.newMessage(for: account, with: jid, timestamp: timestamp, message: encryption.message() ?? data, state: state, remoteChatState: state.direction == .incoming ? chatState : nil) {
|
||||
NotificationCenter.default.post(name: DBChatHistoryStore.MESSAGE_NEW, object: item);
|
||||
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, 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, 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, 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) {
|
||||
NotificationCenter.default.post(name: DBChatHistoryStore.MESSAGE_NEW, object: item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -137,7 +158,7 @@ open class DBChatHistoryStore: Logger {
|
|||
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!];
|
||||
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 {
|
||||
|
@ -149,7 +170,7 @@ open class DBChatHistoryStore: Logger {
|
|||
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!])!;
|
||||
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);
|
||||
|
@ -158,7 +179,7 @@ open class DBChatHistoryStore: Logger {
|
|||
}
|
||||
|
||||
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];
|
||||
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);
|
||||
}
|
||||
|
@ -223,16 +244,7 @@ open class DBChatHistoryStore: Logger {
|
|||
self.itemRemoved(withId: itemId, for: account, with: jid);
|
||||
}
|
||||
}
|
||||
|
||||
open func updateItem(for account: BareJID, with jid: BareJID, id: Int, preview: String?) {
|
||||
dispatcher.async {
|
||||
let params: [String:Any?] = ["id": id, "preview": preview];
|
||||
if try! self.msgUpdatePreviewStmt.update(params) > 0 {
|
||||
self.itemUpdated(withId: id, for: account, with: jid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fileprivate func getMessageIdInt(account: BareJID, jid: BareJID, stanzaId: String?) -> Int? {
|
||||
guard stanzaId != nil else {
|
||||
return nil;
|
||||
|
@ -268,6 +280,46 @@ open class DBChatHistoryStore: Logger {
|
|||
}
|
||||
}
|
||||
|
||||
open func updateItem(for account: BareJID, with jid: BareJID, id: Int, updateAppendix updateFn: @escaping (inout ChatAttachmentAppendix)->Void) {
|
||||
dispatcher.async {
|
||||
var params: [String: Any?] = ["id": id];
|
||||
guard let item = try! self.getChatMessageWithIdStmt.findFirst(params, map: { (cursor) in
|
||||
return self.itemFrom(cursor: cursor, for: account, with: jid)
|
||||
}) as? ChatAttachment else {
|
||||
return;
|
||||
}
|
||||
updateFn(&item.appendix);
|
||||
if let data = try? JSONEncoder().encode(item.appendix), let dataStr = String(data: data, encoding: .utf8) {
|
||||
params["appendix"] = dataStr;
|
||||
try! self.updateItemStmt.update(params)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: DBChatHistoryStore.MESSAGE_UPDATED, object: item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open func updateItem(id: Int, updateAppendix updateFn: @escaping (inout ChatAttachmentAppendix)->Void) {
|
||||
dispatcher.async {
|
||||
var params: [String: Any?] = ["id": id];
|
||||
guard let item = try! self.getChatMessageWithIdStmt.findFirst(params, map: { (cursor) -> ChatViewItemProtocol? in
|
||||
let account: BareJID = cursor["account"]!;
|
||||
let jid: BareJID = cursor["jid"]!;
|
||||
return self.itemFrom(cursor: cursor, for: account, with: jid)
|
||||
}) as? ChatAttachment else {
|
||||
return;
|
||||
}
|
||||
updateFn(&item.appendix);
|
||||
if let data = try? JSONEncoder().encode(item.appendix), let dataStr = String(data: data, encoding: .utf8) {
|
||||
params["appendix"] = dataStr;
|
||||
try! self.updateItemStmt.update(params)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: DBChatHistoryStore.MESSAGE_UPDATED, object: item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open func loadUnsentMessage(for account: BareJID, completionHandler: @escaping (BareJID,BareJID,String,String,MessageEncryption)->Void) {
|
||||
dispatcher.async {
|
||||
try! self.getUnsentMessagesForAccountStmt.query(["account": account] as [String : Any?], forEach: { (cursor) in
|
||||
|
@ -281,7 +333,7 @@ open class DBChatHistoryStore: Logger {
|
|||
}
|
||||
}
|
||||
|
||||
fileprivate func itemUpdated(withId id: Int, for account: BareJID, with jid: BareJID) {
|
||||
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
|
||||
|
@ -306,26 +358,61 @@ open class DBChatHistoryStore: Logger {
|
|||
}
|
||||
}
|
||||
|
||||
@objc open func imageRemovedFromCache(_ notification: NSNotification) {
|
||||
if let data = notification.userInfo {
|
||||
let url = data["url"] as! URL;
|
||||
_ = try! dbConnection.prepareStatement("UPDATE chat_history SET preview = null WHERE preview = ?").update("preview:image:\(url.pathComponents.last!)");
|
||||
public func loadAttachments(for account: BareJID, with jid: BareJID, completionHandler: @escaping ([ChatAttachment])->Void) {
|
||||
let params: [String: Any?] = ["account": account, "jid": jid];
|
||||
dispatcher.async {
|
||||
let attachments: [ChatAttachment] = try! self.getChatAttachmentsStmt.query(params, map: { cursor -> ChatAttachment? in
|
||||
return self.itemFrom(cursor: cursor, for: account, with: jid) as? ChatAttachment;
|
||||
});
|
||||
completionHandler(attachments);
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var linkPreviews: Bool {
|
||||
if #available(iOS 13.0, *) {
|
||||
return Settings.linkPreviews.bool();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func itemFrom(cursor: DBCursor, for account: BareJID, with jid: BareJID) -> ChatViewItemProtocol? {
|
||||
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"]!;
|
||||
let message: String = cursor["data"]!;
|
||||
|
||||
guard let entryType = ItemType(rawValue: cursor["item_type"]!) else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
let authorNickname: String? = cursor["author_nickname"];
|
||||
let authorJid: BareJID? = cursor["author_jid"];
|
||||
let encryption: MessageEncryption = MessageEncryption(rawValue: cursor["encryption"] ?? 0) ?? .none;
|
||||
let encryptionFingerprint: String? = cursor["fingerprint"];
|
||||
let error: String? = cursor["error"];
|
||||
|
||||
var preview: String? = cursor["preview"];
|
||||
return ChatMessage(id: id, timestamp: timestamp, account: account, jid: jid, state: MessageState(rawValue: stateInt)!, message: message, authorNickname: authorNickname, authorJid: authorJid, encryption: encryption, encryptionFingerprint: encryptionFingerprint, preview: preview, error: error);
|
||||
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, 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, 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, encryption: encryption, encryptionFingerprint: encryptionFingerprint, error: error)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func parseAttachmentAppendix(string: String?) -> ChatAttachmentAppendix {
|
||||
guard let data = string?.data(using: .utf8) else {
|
||||
return ChatAttachmentAppendix();
|
||||
}
|
||||
return (try? JSONDecoder().decode(ChatAttachmentAppendix.self, from: data)) ?? ChatAttachmentAppendix();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -393,58 +480,9 @@ public enum MessageDirection: Int {
|
|||
|
||||
public enum ItemType:Int {
|
||||
case message = 0
|
||||
}
|
||||
|
||||
class ChatMessage: ChatViewItemProtocol {
|
||||
|
||||
let id: Int;
|
||||
let timestamp: Date;
|
||||
let account: BareJID;
|
||||
let message: String;
|
||||
let jid: BareJID;
|
||||
let state: MessageState;
|
||||
let authorNickname: String?;
|
||||
let authorJid: BareJID?;
|
||||
let preview: String?;
|
||||
let error: String?;
|
||||
|
||||
let encryption: MessageEncryption;
|
||||
let encryptionFingerprint: String?;
|
||||
|
||||
init(id: Int, timestamp: Date, account: BareJID, jid: BareJID, state: MessageState, message: String, authorNickname: String?, authorJid: BareJID?, encryption: MessageEncryption, encryptionFingerprint: String?, preview: String? = nil, error: String?) {
|
||||
self.id = id;
|
||||
self.timestamp = timestamp;
|
||||
self.account = account;
|
||||
self.message = message;
|
||||
self.jid = jid;
|
||||
self.state = state;
|
||||
self.authorNickname = authorNickname;
|
||||
self.authorJid = authorJid;
|
||||
self.preview = preview;
|
||||
self.encryption = encryption;
|
||||
self.encryptionFingerprint = encryptionFingerprint;
|
||||
self.error = error;
|
||||
}
|
||||
|
||||
func isMergeable(with: ChatViewItemProtocol) -> Bool {
|
||||
guard let item = with as? ChatMessage 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 && abs(self.timestamp.timeIntervalSince(item.timestamp)) < allowedTimeDiff() && self.encryption == item.encryption && self.encryptionFingerprint == item.encryptionFingerprint;
|
||||
}
|
||||
|
||||
func allowedTimeDiff() -> TimeInterval {
|
||||
switch /*Settings.messageGrouping.string() ??*/ "smart" {
|
||||
case "none":
|
||||
return -1.0;
|
||||
case "always":
|
||||
return 60.0 * 60.0 * 24.0;
|
||||
default:
|
||||
return 30.0;
|
||||
}
|
||||
}
|
||||
|
||||
case attachment = 1
|
||||
@available(iOS 13, *)
|
||||
case linkPreview = 2
|
||||
}
|
||||
|
||||
class DeletedMessage: ChatViewItemProtocol {
|
||||
|
@ -468,4 +506,8 @@ class DeletedMessage: ChatViewItemProtocol {
|
|||
return false;
|
||||
}
|
||||
|
||||
func copyText(withTimestamp: Bool, withSender: Bool) -> String? {
|
||||
return nil;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ class AvatarEventHandler: XmppServiceEventHandler {
|
|||
switch event {
|
||||
case let e as PresenceModule.ContactPresenceChanged:
|
||||
NotificationCenter.default.post(name: XmppService.CONTACT_PRESENCE_CHANGED, object: e);
|
||||
guard let photoId = e.presence.vcardTempPhoto, let from = e.presence.from?.bareJid, let to = e.presence.to?.bareJid else {
|
||||
guard let photoId = e.presence.vcardTempPhoto, let from = e.presence.from?.bareJid, let to = e.presence.to?.bareJid, e.presence.findChild(name: "x", xmlns: "http://jabber.org/protocol/muc#user") == nil else {
|
||||
return;
|
||||
}
|
||||
AvatarManager.instance.avatarHashChanged(for: from, on: to, type: .vcardTemp, hash: photoId);
|
||||
|
|
|
@ -92,7 +92,30 @@ class MessageEventHandler: XmppServiceEventHandler {
|
|||
|
||||
let timestamp = e.message.delay?.stamp ?? Date();
|
||||
let state: MessageState = ((e.message.type ?? .chat) == .error) ? .incoming_error_unread : .incoming_unread;
|
||||
DBChatHistoryStore.instance.appendItem(for: account, with: from.bareJid, state: state, type: .message, 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);
|
||||
|
||||
var type: ItemType = .message;
|
||||
if let oob = e.message.oob {
|
||||
if oob == body! {
|
||||
type = .attachment;
|
||||
}
|
||||
}
|
||||
|
||||
DBChatHistoryStore.instance.appendItem(for: account, with: from.bareJid, state: state, 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, 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, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
case let e as MessageDeliveryReceiptsModule.ReceiptEvent:
|
||||
guard let from = e.message.from?.bareJid, let account = e.sessionObject.userBareJid else {
|
||||
return;
|
||||
|
@ -139,11 +162,34 @@ class MessageEventHandler: XmppServiceEventHandler {
|
|||
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);
|
||||
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: state, type: .message, timestamp: timestamp, stanzaId: e.message.id, data: body!, errorCondition: e.message.errorCondition, errorMessage: e.message.errorText, encryption: encryption, encryptionFingerprint: fingerprint, completionHandler: { (msgId) in
|
||||
|
||||
var type: ItemType = .message;
|
||||
if let oob = e.message.oob {
|
||||
if oob == body! {
|
||||
type = .attachment;
|
||||
}
|
||||
}
|
||||
|
||||
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: state, 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, 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, type: .linkPreview, timestamp: timestamp, stanzaId: nil, data: mapUrl.absoluteString, errorCondition: e.message.errorCondition, errorMessage: e.message.errorText, encryption: encryption, encryptionFingerprint: fingerprint, completionHandler: nil);
|
||||
}
|
||||
}
|
||||
}
|
||||
case let e as MessageArchiveManagementModule.ArchivedMessageReceivedEvent:
|
||||
guard let account = e.sessionObject.userBareJid, let from = e.message.from, let to = e.message.to else {
|
||||
return;
|
||||
|
@ -152,10 +198,33 @@ class MessageEventHandler: XmppServiceEventHandler {
|
|||
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!;
|
||||
let state: MessageState = calculateState(direction: account == from.bareJid ? .outgoing : .incoming, error: ((e.message.type ?? .chat) == .error), unread: false);
|
||||
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: state, type: .message, timestamp: timestamp, stanzaId: e.message.id, data: body!, errorCondition: e.message.errorCondition, errorMessage: e.message.errorText, encryption: encryption, encryptionFingerprint: fingerprint, completionHandler: nil);
|
||||
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: state, 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, 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, type: .linkPreview, timestamp: timestamp, stanzaId: nil, data: mapUrl.absoluteString, errorCondition: e.message.errorCondition, errorMessage: e.message.errorText, encryption: encryption, encryptionFingerprint: fingerprint, completionHandler: nil);
|
||||
}
|
||||
}
|
||||
}
|
||||
case let e as OMEMOModule.AvailabilityChangedEvent:
|
||||
NotificationCenter.default.post(name: MessageEventHandler.OMEMO_AVAILABILITY_CHANGED, object: e);
|
||||
default:
|
||||
|
@ -163,75 +232,105 @@ class MessageEventHandler: XmppServiceEventHandler {
|
|||
}
|
||||
}
|
||||
|
||||
static func sendMessage(chat: DBChat, body: String?, url: String?, encrypted: ChatEncryption? = nil, stanzaId: String? = nil) {
|
||||
guard let msg = body ?? url else {
|
||||
return;
|
||||
}
|
||||
|
||||
let encryption = encrypted ?? chat.options.encryption ?? ChatEncryption(rawValue: Settings.messageEncryption.string()!)!;
|
||||
|
||||
let message = chat.createMessage(msg);
|
||||
message.id = stanzaId ?? UUID().uuidString;
|
||||
message.messageDelivery = .request;
|
||||
|
||||
let account = chat.account;
|
||||
let jid = chat.jid.bareJid;
|
||||
|
||||
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, type: .message, timestamp: Date(), stanzaId: message.id, data: msg, encryption: .decrypted, encryptionFingerprint: fingerprint, completionHandler: nil);
|
||||
static func sendAttachment(chat: DBChat, originalUrl: URL?, uploadedUrl: String, appendix: ChatAttachmentAppendix, completionHandler: (()->Void)?) {
|
||||
|
||||
self.sendMessage(chat: chat, body: nil, url: uploadedUrl, chatAttachmentAppendix: appendix, messageStored: { (msgId) in
|
||||
DispatchQueue.main.async {
|
||||
if originalUrl != nil {
|
||||
_ = DownloadStore.instance.store(originalUrl!, filename: originalUrl!.lastPathComponent, with: "\(msgId)");
|
||||
}
|
||||
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());
|
||||
case .failure(let err):
|
||||
let condition = (err is ErrorCondition) ? (err as? ErrorCondition) : nil;
|
||||
guard condition == nil || condition! != .gone else {
|
||||
completionHandler();
|
||||
return;
|
||||
}
|
||||
completionHandler?();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var errorMessage: String? = nil;
|
||||
if let encryptionError = err as? SignalError {
|
||||
switch encryptionError {
|
||||
case .noSession:
|
||||
errorMessage = "There is no trusted device to send message to";
|
||||
default:
|
||||
errorMessage = "It was not possible to send encrypted message due to encryption error";
|
||||
static func sendMessage(chat: DBChat, body: String?, url: String?, encrypted: ChatEncryption? = nil, stanzaId: String? = nil, chatAttachmentAppendix: ChatAttachmentAppendix? = nil, messageStored: ((Int)->Void)? = nil) {
|
||||
guard let msg = body ?? url else {
|
||||
return;
|
||||
}
|
||||
|
||||
let encryption = encrypted ?? chat.options.encryption ?? ChatEncryption(rawValue: Settings.messageEncryption.string()!)!;
|
||||
|
||||
let message = chat.createMessage(msg);
|
||||
message.id = stanzaId ?? UUID().uuidString;
|
||||
message.messageDelivery = .request;
|
||||
|
||||
let account = chat.account;
|
||||
let jid = chat.jid.bareJid;
|
||||
|
||||
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, type: url == nil ? .message : .attachment, timestamp: Date(), stanzaId: message.id, data: msg, encryption: .decrypted, encryptionFingerprint: fingerprint, chatAttachmentAppendix: chatAttachmentAppendix, 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());
|
||||
case .failure(let err):
|
||||
let condition = (err is ErrorCondition) ? (err as? ErrorCondition) : nil;
|
||||
guard condition == nil || condition! != .gone else {
|
||||
completionHandler();
|
||||
return;
|
||||
}
|
||||
|
||||
var errorMessage: String? = nil;
|
||||
if let encryptionError = err as? SignalError {
|
||||
switch encryptionError {
|
||||
case .noSession:
|
||||
errorMessage = "There is no trusted device to send message to";
|
||||
default:
|
||||
errorMessage = "It was not possible to send encrypted message due to encryption error";
|
||||
}
|
||||
}
|
||||
|
||||
DBChatHistoryStore.instance.markOutgoingAsError(for: account, with: jid, stanzaId: message.id!, errorCondition: .undefined_condition, errorMessage: errorMessage);
|
||||
}
|
||||
completionHandler();
|
||||
});
|
||||
});
|
||||
case .none:
|
||||
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);
|
||||
}
|
||||
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);
|
||||
|
||||
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.markOutgoingAsError(for: account, with: jid, stanzaId: message.id!, errorCondition: .undefined_condition, errorMessage: errorMessage);
|
||||
}
|
||||
completionHandler();
|
||||
});
|
||||
});
|
||||
case .none:
|
||||
message.oob = url;
|
||||
if stanzaId == nil {
|
||||
DBChatHistoryStore.instance.appendItem(for: account, with: jid, state: .outgoing_unsent, type: .message, timestamp: Date(), stanzaId: message.id, data: msg, encryption: .none, encryptionFingerprint: nil, completionHandler: nil);
|
||||
}
|
||||
XmppService.instance.tasksQueue.schedule(for: jid, task: { (completionHandler) in
|
||||
sendUnencryptedMessage(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());
|
||||
case .failure(let err):
|
||||
guard let condition = err as? ErrorCondition, condition != .gone else {
|
||||
completionHandler();
|
||||
return;
|
||||
}
|
||||
DBChatHistoryStore.instance.markOutgoingAsError(for: account, with: jid, stanzaId: message.id!, errorCondition: err as? ErrorCondition ?? .undefined_condition, errorMessage: "Could not send message");
|
||||
case .failure(let err):
|
||||
guard let condition = err as? ErrorCondition, condition != .gone else {
|
||||
completionHandler();
|
||||
return;
|
||||
}
|
||||
completionHandler();
|
||||
});
|
||||
DBChatHistoryStore.instance.markOutgoingAsError(for: account, with: jid, stanzaId: message.id!, errorCondition: err as? ErrorCondition ?? .undefined_condition, errorMessage: "Could not send message");
|
||||
}
|
||||
completionHandler();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fileprivate static func sendUnencryptedMessage(_ message: Message, from account: BareJID, completionHandler: @escaping (Result<Void,Error>)->Void) {
|
||||
guard let client = XmppService.instance.getClient(for: account), client.state == .connected else {
|
||||
|
|
|
@ -78,7 +78,29 @@ class MucEventHandler: XmppServiceEventHandler {
|
|||
|
||||
let authorJid = e.nickname == nil ? nil : room.presences[e.nickname!]?.jid?.bareJid;
|
||||
|
||||
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: .message, timestamp: e.timestamp, stanzaId: e.message.id, data: body, encryption: MessageEncryption.none, encryptionFingerprint: nil, completionHandler: nil);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
case let e as MucModule.AbstractOccupantEvent:
|
||||
NotificationCenter.default.post(name: MucEventHandler.ROOM_OCCUPANTS_CHANGED, object: e);
|
||||
case let e as MucModule.PresenceErrorEvent:
|
||||
|
|
|
@ -66,7 +66,6 @@ open class XmppService: Logger, EventHandler {
|
|||
}
|
||||
if applicationState != .active {
|
||||
AvatarManager.instance.clearCache();
|
||||
ImageCache.shared.clearInMemoryCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,11 +23,21 @@ import UIKit
|
|||
|
||||
class ChatSettingsViewController: CustomTableViewController {
|
||||
|
||||
let tree: [[SettingsEnum]] = [
|
||||
[SettingsEnum.recentsMessageLinesNo, SettingsEnum.recentsSortType],
|
||||
[SettingsEnum.sendMessageOnReturn, SettingsEnum.deleteChatHistoryOnClose, SettingsEnum.enableMessageCarbons, SettingsEnum.messageDeliveryReceipts, SettingsEnum.messageEncryption],
|
||||
[SettingsEnum.sharingViaHttpUpload, SettingsEnum.simplifiedLinkToHTTPFile, SettingsEnum.maxImagePreviewSize, SettingsEnum.clearImagePreviewCache],
|
||||
];
|
||||
let tree: [[SettingsEnum]] = {
|
||||
if #available(iOS 13.0, *) {
|
||||
return [
|
||||
[SettingsEnum.recentsMessageLinesNo, SettingsEnum.recentsSortType],
|
||||
[SettingsEnum.sendMessageOnReturn, SettingsEnum.deleteChatHistoryOnClose, SettingsEnum.enableMessageCarbons, SettingsEnum.messageDeliveryReceipts, SettingsEnum.messageEncryption],
|
||||
[SettingsEnum.sharingViaHttpUpload, SettingsEnum.linkPreviews, SettingsEnum.maxImagePreviewSize, SettingsEnum.clearDownloadStore],
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
[SettingsEnum.recentsMessageLinesNo, SettingsEnum.recentsSortType],
|
||||
[SettingsEnum.sendMessageOnReturn, SettingsEnum.deleteChatHistoryOnClose, SettingsEnum.enableMessageCarbons, SettingsEnum.messageDeliveryReceipts, SettingsEnum.messageEncryption],
|
||||
[SettingsEnum.sharingViaHttpUpload, SettingsEnum.maxImagePreviewSize, SettingsEnum.clearDownloadStore],
|
||||
];
|
||||
}
|
||||
}();
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return tree.count;
|
||||
|
@ -106,11 +116,11 @@ class ChatSettingsViewController: CustomTableViewController {
|
|||
return cell;
|
||||
case .maxImagePreviewSize:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "MaxImagePreviewSizeTableViewCell", for: indexPath);
|
||||
(cell.contentView.subviews[1] as! UILabel).text = MaxImagePreviewSizeItem.description(of: Settings.MaxImagePreviewSize.getInt());
|
||||
(cell.contentView.subviews[1] as! UILabel).text = AutoFileDownloadLimit.description(of: Settings.fileDownloadSizeLimit.getInt());
|
||||
cell.accessoryType = .disclosureIndicator;
|
||||
return cell;
|
||||
case .clearImagePreviewCache:
|
||||
return tableView.dequeueReusableCell(withIdentifier: "ClearImagePreviewTableViewCell", for: indexPath);
|
||||
case .clearDownloadStore:
|
||||
return tableView.dequeueReusableCell(withIdentifier: "ClearDownloadStoreTableViewCell", for: indexPath);
|
||||
case .messageDeliveryReceipts:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "MessageDeliveryReceiptsTableViewCell", for: indexPath) as! SwitchTableViewCell;
|
||||
cell.switchView.isOn = Settings.MessageDeliveryReceiptsEnabled.getBool();
|
||||
|
@ -118,12 +128,14 @@ class ChatSettingsViewController: CustomTableViewController {
|
|||
Settings.MessageDeliveryReceiptsEnabled.setValue(switchView.isOn);
|
||||
};
|
||||
return cell;
|
||||
case .simplifiedLinkToHTTPFile:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "SimplifiedLinkToFileTableViewCell", for: indexPath) as! SwitchTableViewCell;
|
||||
cell.switchView.isOn = Settings.SimplifiedLinkToFileIfPreviewIsAvailable.getBool();
|
||||
case .linkPreviews:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "LinkPreviewsTableViewCell", for: indexPath) as! SwitchTableViewCell;
|
||||
if #available(iOS 13.0, *) {
|
||||
cell.switchView.isOn = Settings.linkPreviews.getBool();
|
||||
cell.valueChangedListener = {(switchView: UISwitch) in
|
||||
Settings.SimplifiedLinkToFileIfPreviewIsAvailable.setValue(switchView.isOn);
|
||||
Settings.linkPreviews.setValue(switchView.isOn);
|
||||
};
|
||||
}
|
||||
return cell;
|
||||
case .sendMessageOnReturn:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "SendMessageOnReturnTableViewCell", for: indexPath) as! SwitchTableViewCell;
|
||||
|
@ -162,34 +174,35 @@ class ChatSettingsViewController: CustomTableViewController {
|
|||
case .maxImagePreviewSize:
|
||||
let controller = TablePickerViewController(style: .grouped);
|
||||
let values: [Int] = [0, 1, 2, 4, 8, 10, 15, 30, 50, Int.max];
|
||||
controller.selected = values.firstIndex(of: Settings.MaxImagePreviewSize.getInt() ) ?? 0;
|
||||
controller.selected = values.firstIndex(of: Settings.fileDownloadSizeLimit.getInt() ) ?? 0;
|
||||
controller.items = values.map({ (it)->TablePickerViewItemsProtocol in
|
||||
return MaxImagePreviewSizeItem(value: it);
|
||||
return AutoFileDownloadLimit(value: it);
|
||||
});
|
||||
//controller.selected = 1;
|
||||
controller.onSelectionChange = { (_item) -> Void in
|
||||
let item = _item as! MaxImagePreviewSizeItem;
|
||||
Settings.MaxImagePreviewSize.setValue(item.value);
|
||||
let item = _item as! AutoFileDownloadLimit;
|
||||
Settings.fileDownloadSizeLimit.setValue(item.value);
|
||||
self.tableView.reloadData();
|
||||
};
|
||||
self.navigationController?.pushViewController(controller, animated: true);
|
||||
case .clearImagePreviewCache:
|
||||
let alert = UIAlertController(title: "Image cache", message: "We are using \(ImageCache.shared.diskCacheSize/(1024*1014)) MB of storage.", preferredStyle: .actionSheet);
|
||||
case .clearDownloadStore:
|
||||
let alert = UIAlertController(title: "Download storage", message: "We are using \(DownloadStore.instance.size/(1024*1014)) MB of storage.", preferredStyle: .actionSheet);
|
||||
alert.addAction(UIAlertAction(title: "Flush", style: .destructive, handler: {(action) in
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
ImageCache.shared.emptyDiskCache();
|
||||
DownloadStore.instance.clear();
|
||||
}
|
||||
}));
|
||||
alert.addAction(UIAlertAction(title: "Older than 7 days", style: .destructive, handler: {(action) in
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
ImageCache.shared.emptyDiskCache(olderThan: Date().addingTimeInterval(7*24*60*60.0));
|
||||
DownloadStore.instance.clear(olderThan: Date().addingTimeInterval(7*24*60*60.0*(-1.0)));
|
||||
}
|
||||
}));
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil));
|
||||
alert.popoverPresentationController?.sourceView = self.tableView;
|
||||
alert.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath);
|
||||
|
||||
|
||||
self.present(alert, animated: true, completion: nil);
|
||||
break;
|
||||
case .messageEncryption:
|
||||
let current = ChatEncryption(rawValue: Settings.messageEncryption.getString() ?? "") ?? .none;
|
||||
let controller = TablePickerViewController(style: .grouped);
|
||||
|
@ -218,14 +231,15 @@ class ChatSettingsViewController: CustomTableViewController {
|
|||
case recentsSortType = 3
|
||||
case sharingViaHttpUpload = 4
|
||||
case maxImagePreviewSize = 5;
|
||||
case clearImagePreviewCache = 6;
|
||||
case clearDownloadStore = 6;
|
||||
case messageDeliveryReceipts = 7;
|
||||
case simplifiedLinkToHTTPFile = 8;
|
||||
@available(iOS 13.0, *)
|
||||
case linkPreviews = 8;
|
||||
case sendMessageOnReturn = 9;
|
||||
case messageEncryption = 10;
|
||||
}
|
||||
|
||||
internal class MaxImagePreviewSizeItem: TablePickerViewItemsProtocol {
|
||||
internal class AutoFileDownloadLimit: TablePickerViewItemsProtocol {
|
||||
|
||||
public static func description(of value: Int) -> String {
|
||||
if value == Int.max {
|
||||
|
@ -240,7 +254,7 @@ class ChatSettingsViewController: CustomTableViewController {
|
|||
|
||||
init(value: Int) {
|
||||
self.value = value;
|
||||
self.description = MaxImagePreviewSizeItem.description(of: value);
|
||||
self.description = AutoFileDownloadLimit.description(of: value);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ class ExperimentalSettingsViewController: CustomTableViewController {
|
|||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return 5;
|
||||
return 4;
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
|
@ -48,13 +48,6 @@ class ExperimentalSettingsViewController: CustomTableViewController {
|
|||
Settings.enableBookmarksSync.setValue(switchView.isOn);
|
||||
}
|
||||
return cell;
|
||||
case .enableNewUI:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "EnableNewUITableViewCell", for: indexPath) as! SwitchTableViewCell;
|
||||
cell.switchView.isOn = Settings.EnableNewUI.getBool();
|
||||
cell.valueChangedListener = {(switchView: UISwitch) in
|
||||
Settings.EnableNewUI.setValue(switchView.isOn);
|
||||
}
|
||||
return cell;
|
||||
case .enableMarkdown:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "EnableMarkdownTableViewCell", for: indexPath) as! SwitchTableViewCell;
|
||||
cell.switchView.isOn = Settings.EnableMarkdownFormatting.getBool();
|
||||
|
@ -84,8 +77,7 @@ class ExperimentalSettingsViewController: CustomTableViewController {
|
|||
internal enum SettingsEnum: Int {
|
||||
case notificationsFromUnknown = 0
|
||||
case enableBookmarksSync = 1
|
||||
case enableNewUI = 2
|
||||
case enableMarkdown = 3
|
||||
case showEmoticons = 4
|
||||
case enableMarkdown = 2
|
||||
case showEmoticons = 3
|
||||
}
|
||||
}
|
||||
|
|
|
@ -160,7 +160,6 @@ class SettingsViewController: CustomTableViewController {
|
|||
return cell;
|
||||
} else {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "AboutSettingsViewCell", for: indexPath);
|
||||
cell.accessoryType = .disclosureIndicator;
|
||||
return cell;
|
||||
}
|
||||
}
|
||||
|
|
303
SiskinIM/util/DownloadManager.swift
Normal file
303
SiskinIM/util/DownloadManager.swift
Normal file
|
@ -0,0 +1,303 @@
|
|||
//
|
||||
// DownloadManager.swift
|
||||
//
|
||||
// Siskin IM
|
||||
// Copyright (C) 2019 "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 MobileCoreServices
|
||||
import TigaseSwift
|
||||
|
||||
class DownloadManager {
|
||||
|
||||
static let instance = DownloadManager();
|
||||
|
||||
private let dispatcher = QueueDispatcher(label: "download_manager_queue");
|
||||
|
||||
private var inProgress: [URL: Item] = [:];
|
||||
|
||||
private var itemDownloadInProgress: [Int] = [];
|
||||
|
||||
func downloadInProgress(for url: URL, completionHandler: @escaping (Result<String,DownloadError>)->Void) -> Bool {
|
||||
return dispatcher.sync {
|
||||
if let item = self.inProgress[url] {
|
||||
item.onCompletion(completionHandler);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
func downloadInProgress(for item: ChatAttachment) -> Bool {
|
||||
return dispatcher.sync {
|
||||
return self.itemDownloadInProgress.contains(item.id);
|
||||
}
|
||||
}
|
||||
|
||||
func download(item: ChatAttachment, maxSize: Int64) -> Bool {
|
||||
return dispatcher.sync {
|
||||
guard !itemDownloadInProgress.contains(item.id) else {
|
||||
return false;
|
||||
}
|
||||
|
||||
itemDownloadInProgress.append(item.id);
|
||||
|
||||
if let hash = Digest.sha1.digest(toHex: item.url.data(using: .utf8)!), var params = Settings.sharedDefaults!.dictionary(forKey: "upload-\(hash)"), let filename = params["name"] as? String {
|
||||
var jids: [BareJID] = (params["jids"] as? [String])?.map({ BareJID($0) }) ?? [];
|
||||
|
||||
let sharedFileUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.siskinim.shared")!.appendingPathComponent("upload", isDirectory: true).appendingPathComponent(hash, isDirectory: false);
|
||||
|
||||
var handled = false;
|
||||
if jids.contains(item.jid) {
|
||||
jids = jids.filter({ (j) -> Bool in
|
||||
return j != item.jid;
|
||||
});
|
||||
params["jids"] = jids.map({ $0.stringValue });
|
||||
|
||||
DownloadStore.instance.store(sharedFileUrl, filename: filename, with: "\(item.id)");
|
||||
DBChatHistoryStore.instance.updateItem(for: item.account, with: item.jid, id: item.id, updateAppendix: { appendix in
|
||||
appendix.filesize = params["size"] as? Int;
|
||||
appendix.mimetype = params["mimeType"] as? String;
|
||||
appendix.filename = filename;
|
||||
appendix.state = .downloaded;
|
||||
});
|
||||
handled = true;
|
||||
}
|
||||
|
||||
if jids.isEmpty || !FileManager.default.fileExists(atPath: sharedFileUrl.path) {
|
||||
Settings.sharedDefaults?.removeObject(forKey: "upload-\(hash)")
|
||||
if FileManager.default.fileExists(atPath: sharedFileUrl.path) {
|
||||
try! FileManager.default.removeItem(at: sharedFileUrl);
|
||||
}
|
||||
} else {
|
||||
Settings.sharedDefaults?.set(params, forKey: "upload-\(hash)");
|
||||
}
|
||||
guard !handled else {
|
||||
self.itemDownloadInProgress = self.itemDownloadInProgress.filter({ (id) -> Bool in
|
||||
return item.id != id;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
let url = URL(string: item.url)!;
|
||||
|
||||
let sessionConfig = URLSessionConfiguration.default;
|
||||
let session = URLSession(configuration: sessionConfig);
|
||||
DownloadManager.retrieveHeaders(session: session, url: url, completionHandler: { headersResult in
|
||||
switch headersResult {
|
||||
case .success(let suggestedFilename, let expectedSize, let mimeType):
|
||||
let isTooBig = expectedSize > maxSize;
|
||||
|
||||
DBChatHistoryStore.instance.updateItem(for: item.account, with: item.jid, id: item.id, updateAppendix: { appendix in
|
||||
appendix.filesize = Int(expectedSize);
|
||||
appendix.mimetype = mimeType;
|
||||
appendix.filename = suggestedFilename;
|
||||
if isTooBig {
|
||||
appendix.state = .tooBig;
|
||||
}
|
||||
});
|
||||
|
||||
guard !isTooBig else {
|
||||
self.dispatcher.async {
|
||||
self.itemDownloadInProgress = self.itemDownloadInProgress.filter({ (id) -> Bool in
|
||||
return item.id != id;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
DownloadManager.download(session: session, url: url, completionHandler: { result in
|
||||
switch result {
|
||||
case .success(let localUrl, let filename):
|
||||
//let id = UUID().uuidString;
|
||||
DownloadStore.instance.store(localUrl, filename: filename, with: "\(item.id)");
|
||||
DBChatHistoryStore.instance.updateItem(for: item.account, with: item.jid, id: item.id, updateAppendix: { appendix in
|
||||
appendix.state = .downloaded;
|
||||
});
|
||||
self.dispatcher.sync {
|
||||
self.itemDownloadInProgress = self.itemDownloadInProgress.filter({ (id) -> Bool in
|
||||
return item.id != id;
|
||||
});
|
||||
}
|
||||
case .failure(let err):
|
||||
var statusCode = 0;
|
||||
switch err {
|
||||
case .responseError(let code):
|
||||
statusCode = code;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
DBChatHistoryStore.instance.updateItem(for: item.account, with: item.jid, id: item.id, updateAppendix: { appendix in
|
||||
appendix.state = statusCode == 404 ? .gone : .error;
|
||||
});
|
||||
self.dispatcher.sync {
|
||||
self.itemDownloadInProgress = self.itemDownloadInProgress.filter({ (id) -> Bool in
|
||||
return item.id != id;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
case .failure(let statusCode):
|
||||
DBChatHistoryStore.instance.updateItem(for: item.account, with: item.jid, id: item.id, updateAppendix: { appendix in
|
||||
appendix.state = statusCode == 404 ? .gone : .error;
|
||||
});
|
||||
self.dispatcher.async {
|
||||
self.itemDownloadInProgress = self.itemDownloadInProgress.filter({ (id) -> Bool in
|
||||
return item.id != id;
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
func downloadFile(destination: DownloadStore, as id: String, url: URL, maxSize: Int64, excludedMimetypes: [String], completionHandler: @escaping (Result<String,DownloadError>)->Void) {
|
||||
|
||||
dispatcher.async {
|
||||
if let item = self.inProgress[url] {
|
||||
item.onCompletion(completionHandler);
|
||||
} else {
|
||||
let item = Item();
|
||||
item.onCompletion(completionHandler);
|
||||
self.inProgress[url] = item;
|
||||
|
||||
self.downloadFile(url: url, maxSize: maxSize, excludedMimetypes: excludedMimetypes) { (result) in
|
||||
self.dispatcher.async {
|
||||
switch result {
|
||||
case .success(let localUrl, let filename):
|
||||
//let id = UUID().uuidString;
|
||||
destination.store(localUrl, filename: filename, with: id);
|
||||
item.completed(with: .success(id))
|
||||
case .failure(let err):
|
||||
item.completed(with: .failure(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downloadFile(url: URL, maxSize: Int64, excludedMimetypes: [String], completionHandler: @escaping (Result<(URL,String),DownloadError>)->Void) {
|
||||
let sessionConfig = URLSessionConfiguration.default;
|
||||
let session = URLSession(configuration: sessionConfig);
|
||||
|
||||
DownloadManager.retrieveHeaders(session: session, url: url, completionHandler: { headersResult in
|
||||
switch headersResult {
|
||||
case .success(let suggestedFilename, let expectedSize, let mimeType):
|
||||
if let type = mimeType {
|
||||
guard !excludedMimetypes.contains(type) else {
|
||||
completionHandler(.failure(.badMimeType(mimeType: type)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
DownloadManager.download(session: session, url: url, completionHandler: completionHandler);
|
||||
break;
|
||||
case .failure(let statusCode):
|
||||
completionHandler(.failure(.responseError(statusCode: statusCode)));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static func download(session: URLSession, url: URL, completionHandler: @escaping (Result<(URL,String), DownloadError>)->Void) {
|
||||
let request = URLRequest(url: url);
|
||||
let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
|
||||
if let tempLocalUrl = tempLocalUrl, error == nil {
|
||||
if let filename = response?.suggestedFilename {
|
||||
completionHandler(.success((tempLocalUrl, filename)));
|
||||
} else if let mimeType = response?.mimeType, let filenameExt = DownloadManager.mimeTypeToExtension(mimeType: mimeType) {
|
||||
completionHandler(.success((tempLocalUrl, "file.\(filenameExt)")));
|
||||
} else if let uti = try? tempLocalUrl.resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier, let filenameExt = UTTypeCopyPreferredTagWithClass(uti as CFString, kUTTagClassFilenameExtension)?.takeRetainedValue() as String? {
|
||||
completionHandler(.success((tempLocalUrl, "file.\(filenameExt)")));
|
||||
} else {
|
||||
completionHandler(.success((tempLocalUrl, tempLocalUrl.lastPathComponent)));
|
||||
}
|
||||
} else {
|
||||
guard error == nil else {
|
||||
completionHandler(.failure(.networkError(error: error!)));
|
||||
return;
|
||||
}
|
||||
|
||||
completionHandler(.failure(.responseError(statusCode: (response as? HTTPURLResponse)?.statusCode ?? 500)));
|
||||
}
|
||||
}
|
||||
task.resume();
|
||||
}
|
||||
|
||||
static func mimeTypeToExtension(mimeType: String) -> String? {
|
||||
let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType as CFString, nil)
|
||||
guard let fileUTI = uti?.takeRetainedValue(),
|
||||
let fileExtension = UTTypeCopyPreferredTagWithClass(fileUTI, kUTTagClassFilenameExtension) else { return nil }
|
||||
|
||||
let extensionString = String(fileExtension.takeRetainedValue())
|
||||
return extensionString
|
||||
}
|
||||
|
||||
static func retrieveHeaders(session: URLSession, url: URL, completionHandler: @escaping (HeadersResult)->Void) {
|
||||
var request = URLRequest(url: url);
|
||||
request.httpMethod = "HEAD";
|
||||
session.dataTask(with: request) { (data, resp, error) in
|
||||
guard let response = resp as? HTTPURLResponse else {
|
||||
completionHandler(.failure(statusCode: 500));
|
||||
return;
|
||||
}
|
||||
|
||||
switch response.statusCode {
|
||||
case 200:
|
||||
completionHandler(.success(suggestedFilename: response.suggestedFilename, expectedSize: response.expectedContentLength, mimeType: response.mimeType))
|
||||
default:
|
||||
completionHandler(.failure(statusCode: response.statusCode));
|
||||
}
|
||||
}.resume();
|
||||
}
|
||||
|
||||
class Item {
|
||||
let operationQueue = OperationQueue();
|
||||
var result: Result<String,DownloadError>? = nil;
|
||||
|
||||
init() {
|
||||
self.operationQueue.isSuspended = true;
|
||||
}
|
||||
|
||||
func onCompletion(_ completionHandler: @escaping (Result<String,DownloadError>)->Void) {
|
||||
operationQueue.addOperation {
|
||||
completionHandler(self.result ?? .failure(DownloadError.responseError(statusCode: 500)));
|
||||
}
|
||||
}
|
||||
|
||||
func completed(with result: Result<String,DownloadError>?) {
|
||||
self.result = result;
|
||||
operationQueue.isSuspended = false;
|
||||
}
|
||||
}
|
||||
|
||||
enum HeadersResult {
|
||||
case success(suggestedFilename: String?, expectedSize: Int64, mimeType: String?)
|
||||
case failure(statusCode: Int)
|
||||
}
|
||||
|
||||
enum DownloadError: Error {
|
||||
case networkError(error: Error)
|
||||
case responseError(statusCode: Int)
|
||||
case tooBig(size: Int64, mimeType: String?, filename: String?)
|
||||
case badMimeType(mimeType: String?)
|
||||
}
|
||||
}
|
118
SiskinIM/util/DownloadStore.swift
Normal file
118
SiskinIM/util/DownloadStore.swift
Normal file
|
@ -0,0 +1,118 @@
|
|||
//
|
||||
// DownloadStore.swift
|
||||
//
|
||||
// Siskin IM
|
||||
// Copyright (C) 2019 "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 UIKit
|
||||
import TigaseSwift
|
||||
|
||||
class DownloadStore {
|
||||
|
||||
static let instance = DownloadStore();
|
||||
|
||||
fileprivate let queue = DispatchQueue(label: "download_store_queue");
|
||||
|
||||
let diskCacheUrl: URL;
|
||||
|
||||
//let cache = NSCache<NSString, NSImage>();
|
||||
var size: Int {
|
||||
guard let enumerator: FileManager.DirectoryEnumerator = try? FileManager.default.enumerator(at: diskCacheUrl, includingPropertiesForKeys: [.totalFileAllocatedSizeKey, .isRegularFileKey]) else {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return enumerator.map({ $0 as! URL}).filter({ (try? $0.resourceValues(forKeys: [.isRegularFileKey]))?.isRegularFile ?? false}).map({ (try? $0.resourceValues(forKeys: [.totalFileAllocatedSizeKey]))?.totalFileAllocatedSize ?? 0}).reduce(0, +);
|
||||
// for enu
|
||||
// return (.map { url -> Int in
|
||||
// return (try? url.resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize ?? 0) ?? 0;
|
||||
// }.reduce(0, +)) ?? 0;
|
||||
}
|
||||
|
||||
init() {
|
||||
diskCacheUrl = try! FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("download", isDirectory: true);
|
||||
if !FileManager.default.fileExists(atPath: diskCacheUrl.path) {
|
||||
try! FileManager.default.createDirectory(at: diskCacheUrl, withIntermediateDirectories: true, attributes: nil);
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(messageRemoved(_:)), name: DBChatHistoryStore.MESSAGE_REMOVED, object: nil);
|
||||
}
|
||||
|
||||
@objc func messageRemoved(_ notification: Notification) {
|
||||
guard let item = notification.object as? DeletedMessage else {
|
||||
return;
|
||||
}
|
||||
self.deleteFile(for: "\(item.id)")
|
||||
}
|
||||
|
||||
func clear(olderThan: Date? = nil) {
|
||||
guard let messageDirs = try? FileManager.default.contentsOfDirectory(at: diskCacheUrl, includingPropertiesForKeys: [.creationDateKey], options: .skipsSubdirectoryDescendants) else {
|
||||
return;
|
||||
}
|
||||
for dir in messageDirs {
|
||||
if olderThan == nil || (((try? dir.resourceValues(forKeys: [.creationDateKey]).creationDate) ?? Date()) < olderThan!), let id = Int(dir.lastPathComponent) {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: dir);
|
||||
DBChatHistoryStore.instance.updateItem(id: id, updateAppendix: { appendix in
|
||||
appendix.state = .removed;
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func store(_ source: URL, filename: String, with id: String) -> URL {
|
||||
let fileDir = diskCacheUrl.appendingPathComponent(id, isDirectory: true);
|
||||
if !FileManager.default.fileExists(atPath: fileDir.path) {
|
||||
try! FileManager.default.createDirectory(at: fileDir, withIntermediateDirectories: true, attributes: nil);
|
||||
}
|
||||
|
||||
try? FileManager.default.copyItem(at: source, to: fileDir.appendingPathComponent(filename));
|
||||
if !FileManager.default.fileExists(atPath: fileDir.appendingPathComponent(id).path) {
|
||||
try! FileManager.default.createSymbolicLink(at: fileDir.appendingPathComponent(id), withDestinationURL: fileDir.appendingPathComponent(filename));
|
||||
}
|
||||
|
||||
return fileDir.appendingPathComponent(filename);
|
||||
}
|
||||
|
||||
func url(for id: String) -> URL? {
|
||||
let linkUrl = diskCacheUrl.appendingPathComponent(id, isDirectory: true).appendingPathComponent(id);
|
||||
|
||||
guard let filePath = try? FileManager.default.destinationOfSymbolicLink(atPath: linkUrl.path) else {
|
||||
print("link exists:", linkUrl.path, FileManager.default.fileExists(atPath: linkUrl.path));
|
||||
return nil;
|
||||
}
|
||||
|
||||
let filename = URL(fileURLWithPath: filePath).lastPathComponent;
|
||||
|
||||
// print("got link:", linkUrl, "path:", linkUrl.resolvingSymlinksInPath().path, "path to file:", filePath, "exists:", FileManager.default.fileExists(atPath: filePath), FileManager.default.fileExists(atPath: diskCacheUrl.appendingPathComponent(id, isDirectory: true).path));
|
||||
let realPathToFile = diskCacheUrl.appendingPathComponent(id, isDirectory: true).appendingPathComponent(filename);
|
||||
print("got link:", linkUrl, "path:", linkUrl.resolvingSymlinksInPath().path, "path to file:", realPathToFile.path, "exists:", FileManager.default.fileExists(atPath: realPathToFile.path), FileManager.default.fileExists(atPath: diskCacheUrl.appendingPathComponent(id, isDirectory: true).path));
|
||||
return realPathToFile;
|
||||
}
|
||||
|
||||
func deleteFile(for id: String) {
|
||||
let fileDir = diskCacheUrl.appendingPathComponent(id, isDirectory: true);
|
||||
guard FileManager.default.fileExists(atPath: fileDir.path) else {
|
||||
return;
|
||||
}
|
||||
|
||||
try? FileManager.default.removeItem(at: fileDir);
|
||||
}
|
||||
|
||||
}
|
|
@ -20,154 +20,83 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import Shared
|
||||
import TigaseSwift
|
||||
import MobileCoreServices
|
||||
|
||||
class ImageCache {
|
||||
|
||||
public static let DISK_CACHE_IMAGE_REMOVED = Notification.Name("DISK_CACHE.IMAGE_REMOVED");
|
||||
|
||||
static let shared = ImageCache();
|
||||
|
||||
fileprivate let diskCacheUrl: URL = {
|
||||
let tmp = try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("images", isDirectory: true);
|
||||
if !FileManager.default.fileExists(atPath: tmp.path) {
|
||||
try? FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true, attributes: nil);
|
||||
static func convertToAttachments() {
|
||||
// converting ImageCache!!!
|
||||
let diskCacheUrl = try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("images", isDirectory: true);
|
||||
guard FileManager.default.fileExists(atPath: diskCacheUrl.path) else {
|
||||
return;
|
||||
}
|
||||
return tmp;
|
||||
}();
|
||||
|
||||
|
||||
fileprivate let cache: NSCache<NSString,ImageHolder> = {
|
||||
let tmp = NSCache<NSString,ImageHolder>();
|
||||
tmp.countLimit = 20;
|
||||
tmp.totalCostLimit = 20 * 1024 * 1024;
|
||||
return tmp;
|
||||
}();
|
||||
|
||||
var diskCacheSize: Int {
|
||||
var size: Int = 0;
|
||||
try? FileManager.default.contentsOfDirectory(at: diskCacheUrl, includingPropertiesForKeys: [.fileAllocatedSizeKey], options: .skipsSubdirectoryDescendants).forEach { (url) in
|
||||
size = size + ((try? FileManager.default.attributesOfItem(atPath: url.path)[FileAttributeKey.size] as? Int) ?? 0)!;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
func emptyDiskCache(olderThan: Date? = nil) {
|
||||
try? FileManager.default.contentsOfDirectory(at: diskCacheUrl, includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants).forEach({ (url) in
|
||||
if olderThan != nil {
|
||||
let creationDate = (try? FileManager.default.attributesOfItem(atPath: url.path))?[FileAttributeKey.creationDate] as? Date;
|
||||
if (creationDate == nil) || (creationDate!.compare(olderThan!) == .orderedDescending) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
try? FileManager.default.removeItem(at: url);
|
||||
cache.removeObject(forKey: url.pathComponents.last! as NSString);
|
||||
NotificationCenter.default.post(name: ImageCache.DISK_CACHE_IMAGE_REMOVED, object: self, userInfo: ["url": url]);
|
||||
|
||||
let previewsToConvert = try! DBConnection.main.prepareStatement("SELECT id FROM chat_history WHERE preview IS NOT NULL").query(map: { cursor -> Int in
|
||||
return cursor["id"]!;
|
||||
});
|
||||
}
|
||||
|
||||
func clearInMemoryCache() {
|
||||
cache.removeAllObjects();
|
||||
}
|
||||
|
||||
func getURL(for key: String?) -> URL? {
|
||||
guard key != nil else {
|
||||
return nil;
|
||||
}
|
||||
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 = ?");
|
||||
|
||||
return diskCacheUrl.appendingPathComponent(key!);
|
||||
}
|
||||
|
||||
func get(for key: String?, ifMissing: (()->Void)?) -> UIImage? {
|
||||
guard key != nil else {
|
||||
ifMissing?();
|
||||
return nil;
|
||||
}
|
||||
let val = cache.object(forKey: key! as NSString);
|
||||
if val?.beginContentAccess() ?? false {
|
||||
defer {
|
||||
val?.endContentAccess();
|
||||
}
|
||||
return val!.image;
|
||||
}
|
||||
let image = UIImage(contentsOfFile: diskCacheUrl.appendingPathComponent(key!).path);
|
||||
if image == nil {
|
||||
ifMissing?();
|
||||
} else {
|
||||
cache.setObject(ImageHolder(image: image!), forKey: key! as NSString);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
func set(image value: UIImage, completion: ((String)->Void)? = nil) {
|
||||
let key = "\(UUID().description).jpg";
|
||||
cache.setObject(ImageHolder(image: value), forKey: key as NSString);
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
if let data = value.jpegData(compressionQuality: 1.0) {
|
||||
let newUrl = self.diskCacheUrl.appendingPathComponent(key);
|
||||
_ = FileManager.default.createFile(atPath: newUrl.path, contents: data, attributes: nil);
|
||||
completion?(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func set(url value: URL, mimeType: String? = nil, completion: ((String)->Void)? = nil) {
|
||||
let uti = mimeType != nil ? UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType! as CFString, kUTTypeImage)?.takeRetainedValue() : nil;
|
||||
let ext = uti != nil ? UTTypeCopyPreferredTagWithClass(uti! as CFString, kUTTagClassFilenameExtension)?.takeRetainedValue() : nil;
|
||||
let key = ext == nil ? UUID().description : "\(UUID().description).\(ext!)";
|
||||
let newUrl = diskCacheUrl.appendingPathComponent(key);
|
||||
try? FileManager.default.copyItem(at: value, to: newUrl);
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
if let image = UIImage(contentsOfFile: newUrl.path) {
|
||||
self.cache.setObject(ImageHolder(image: image), forKey: key as NSString);
|
||||
completion?(key);
|
||||
} else {
|
||||
print("no image from copied URL", value, newUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class ImageHolder: NSDiscardableContent {
|
||||
|
||||
var counter = 0;
|
||||
var image: UIImage!;
|
||||
|
||||
fileprivate init?(data: NSData?) {
|
||||
guard data != nil else {
|
||||
return nil;
|
||||
let group = DispatchGroup();
|
||||
group.enter();
|
||||
|
||||
for id in previewsToConvert {
|
||||
guard let (item, preview, stanzaId) = try! convertStmt.findFirst(id, map: { (cursor) -> (ChatMessage, 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 preview: String = cursor["preview"] else {
|
||||
return nil;
|
||||
}
|
||||
return (item, preview, stanzaId);
|
||||
}) else {
|
||||
return;
|
||||
}
|
||||
|
||||
image = UIImage(data: data! as Data);
|
||||
guard image != nil else {
|
||||
return nil;
|
||||
if preview.hasPrefix("preview:image:"), item.error != nil {
|
||||
let url = diskCacheUrl.appendingPathComponent(String(preview.dropFirst(14)));
|
||||
if FileManager.default.fileExists(atPath: url.path) {
|
||||
group.enter();
|
||||
var appendix = ChatAttachmentAppendix();
|
||||
var filename = "image.jpg";
|
||||
if let values = try? url.resourceValues(forKeys: [.typeIdentifierKey, .fileSizeKey]) {
|
||||
if let uti = values.typeIdentifier {
|
||||
appendix.mimetype = UTTypeCopyPreferredTagWithClass(uti as CFString, kUTTagClassMIMEType)?.takeRetainedValue() as String?;
|
||||
if let ext = UTTypeCopyPreferredTagWithClass(kUTTagClassFilenameExtension, uti as CFString)?.takeRetainedValue() as String? {
|
||||
filename = "image.\(ext)";
|
||||
}
|
||||
}
|
||||
appendix.filesize = values.fileSize;
|
||||
}
|
||||
appendix.filename = filename;
|
||||
|
||||
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
|
||||
DownloadStore.instance.store(url, filename: filename, with: "\(newId)");
|
||||
if isAttachmentOnly {
|
||||
DBChatHistoryStore.instance.removeItem(for: item.account, with: item.jid, itemId: item.id);
|
||||
} else {
|
||||
try! removePreviewStmt.update(item.id);
|
||||
}
|
||||
try? FileManager.default.removeItem(at: url);
|
||||
group.leave();
|
||||
});
|
||||
} else {
|
||||
try! removePreviewStmt.update(item.id);
|
||||
}
|
||||
} else {
|
||||
try! removePreviewStmt.update(item.id);
|
||||
}
|
||||
}
|
||||
group.notify(queue: DispatchQueue.main, execute: {
|
||||
try? FileManager.default.removeItem(at: diskCacheUrl);
|
||||
})
|
||||
|
||||
fileprivate init(image: UIImage) {
|
||||
self.image = image;
|
||||
}
|
||||
|
||||
@objc fileprivate func discardContentIfPossible() {
|
||||
if counter == 0 {
|
||||
image = nil;
|
||||
}
|
||||
}
|
||||
|
||||
@objc fileprivate func isContentDiscarded() -> Bool {
|
||||
return image == nil;
|
||||
}
|
||||
|
||||
@objc fileprivate func beginContentAccess() -> Bool {
|
||||
guard !isContentDiscarded() else {
|
||||
return false;
|
||||
}
|
||||
counter += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
@objc fileprivate func endContentAccess() {
|
||||
counter -= 1;
|
||||
}
|
||||
group.leave();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
102
SiskinIM/util/MetadataCache.swift
Normal file
102
SiskinIM/util/MetadataCache.swift
Normal file
|
@ -0,0 +1,102 @@
|
|||
//
|
||||
// MetadataCache.swift
|
||||
//
|
||||
// Siskin IM
|
||||
// Copyright (C) 2019 "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 LinkPresentation
|
||||
import TigaseSwift
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
class MetadataCache {
|
||||
|
||||
static let instance = MetadataCache();
|
||||
|
||||
private var cache: [URL: Result<LPLinkMetadata, MetadataCache.CacheError>] = [:];
|
||||
private let diskCacheUrl: URL;
|
||||
private let dispatcher = QueueDispatcher(label: "MetadataCache");
|
||||
|
||||
private var inProgress: [URL: OperationQueue] = [:];
|
||||
|
||||
init() {
|
||||
diskCacheUrl = try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("metadata", isDirectory: true);
|
||||
if !FileManager.default.fileExists(atPath: diskCacheUrl.path) {
|
||||
try! FileManager.default.createDirectory(at: diskCacheUrl, withIntermediateDirectories: true, attributes: nil);
|
||||
}
|
||||
}
|
||||
|
||||
func store(_ value: LPLinkMetadata, for id: String) {
|
||||
let fileUrl = diskCacheUrl.appendingPathComponent("\(id).metadata");
|
||||
guard let codedData = try? NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: false) else {
|
||||
return;
|
||||
}
|
||||
|
||||
try? codedData.write(to: fileUrl);
|
||||
}
|
||||
|
||||
func metadata(for id: String) -> LPLinkMetadata? {
|
||||
guard let data = FileManager.default.contents(atPath: diskCacheUrl.appendingPathComponent("\(id).metadata").path) else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return try! NSKeyedUnarchiver.unarchivedObject(ofClass: LPLinkMetadata.self, from: data);
|
||||
}
|
||||
|
||||
func generateMetadata(for url: URL, withId id: String, completionHandler: @escaping (LPLinkMetadata?)->Void) {
|
||||
dispatcher.async {
|
||||
if let queue = self.inProgress[url] {
|
||||
queue.addOperation {
|
||||
completionHandler(self.metadata(for: id));
|
||||
}
|
||||
} else {
|
||||
let queue = OperationQueue();
|
||||
queue.isSuspended = true;
|
||||
self.inProgress[url] = queue;
|
||||
|
||||
queue.addOperation {
|
||||
completionHandler(self.metadata(for: id));
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let provider = LPMetadataProvider();
|
||||
provider.startFetchingMetadata(for: url, completionHandler: { (meta, error) in
|
||||
if let metadata = meta {
|
||||
self.store(metadata, for: id);
|
||||
} else {
|
||||
print("failed to download metadata for:", url);
|
||||
let metadata = LPLinkMetadata();
|
||||
metadata.originalURL = url;
|
||||
self.store(metadata, for: id);
|
||||
}
|
||||
self.dispatcher.async {
|
||||
self.inProgress.removeValue(forKey: url);
|
||||
queue.isSuspended = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CacheError: Error {
|
||||
case NO_DATA
|
||||
case RETRIEVAL_ERROR
|
||||
}
|
||||
}
|
||||
|
|
@ -38,42 +38,46 @@ public enum Settings: String {
|
|||
case RecentsMessageLinesNo
|
||||
case RecentsOrder
|
||||
case SharingViaHttpUpload
|
||||
case MaxImagePreviewSize
|
||||
//case MaxImagePreviewSize
|
||||
case fileDownloadSizeLimit
|
||||
case MessageDeliveryReceiptsEnabled
|
||||
case SimplifiedLinkToFileIfPreviewIsAvailable
|
||||
//case SimplifiedLinkToFileIfPreviewIsAvailable
|
||||
case SendMessageOnReturn
|
||||
case CopyMessagesWithTimestamps
|
||||
case XmppPipelining
|
||||
case AppearanceTheme
|
||||
case enableBookmarksSync
|
||||
case messageEncryption
|
||||
case EnableNewUI = "new-ui"
|
||||
case EnableMarkdownFormatting = "markdown"
|
||||
case ShowEmoticons
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
case linkPreviews
|
||||
|
||||
public static let SETTINGS_CHANGED = Notification.Name("settingsChanged");
|
||||
|
||||
fileprivate static var store: UserDefaults {
|
||||
return UserDefaults.standard;
|
||||
}
|
||||
|
||||
fileprivate static var sharedDefaults = UserDefaults(suiteName: "group.TigaseMessenger.Share");
|
||||
public static let sharedDefaults = UserDefaults(suiteName: "group.TigaseMessenger.Share");
|
||||
|
||||
public static func initialize() {
|
||||
let defaults: [String: AnyObject] = [
|
||||
"DeleteChatHistoryOnChatClose" : false as AnyObject,
|
||||
"enableMessageCarbons" : true as AnyObject,
|
||||
"RosterType" : "flat" as AnyObject,
|
||||
"RosterItemsOrder" : RosterSortingOrder.alphabetical.rawValue as AnyObject,
|
||||
"RosterAvailableOnly" : false as AnyObject,
|
||||
"RosterDisplayHiddenGroup" : false as AnyObject,
|
||||
"AutoSubscribeOnAcceptedSubscriptionRequest" : false as AnyObject,
|
||||
"NotificationsFromUnknown" : true as AnyObject,
|
||||
"RecentsMessageLinesNo" : 2 as AnyObject,
|
||||
"RecentsOrder" : "byTime" as AnyObject,
|
||||
"SendMessageOnReturn" : true as AnyObject,
|
||||
"AppearanceTheme": "classic" as AnyObject,
|
||||
"messageEncryption": "none" as AnyObject
|
||||
let defaults: [String: Any] = [
|
||||
"DeleteChatHistoryOnChatClose" : false,
|
||||
"enableMessageCarbons" : true,
|
||||
"RosterType" : "flat",
|
||||
"RosterItemsOrder" : RosterSortingOrder.alphabetical.rawValue,
|
||||
"RosterAvailableOnly" : false,
|
||||
"RosterDisplayHiddenGroup" : false,
|
||||
"AutoSubscribeOnAcceptedSubscriptionRequest" : false,
|
||||
"NotificationsFromUnknown" : true,
|
||||
"RecentsMessageLinesNo" : 2,
|
||||
"RecentsOrder" : "byTime",
|
||||
"SendMessageOnReturn" : true,
|
||||
"AppearanceTheme": "classic",
|
||||
"messageEncryption": "none",
|
||||
"linkPreviews": true
|
||||
];
|
||||
store.register(defaults: defaults);
|
||||
["EnableMessageCarbons": Settings.enableMessageCarbons, "MessageEncryption": .messageEncryption, "EnableBookmarksSync": Settings.enableBookmarksSync].forEach { (oldKey, newKey) in
|
||||
|
@ -82,6 +86,12 @@ public enum Settings: String {
|
|||
store.set(val, forKey: newKey.rawValue)
|
||||
}
|
||||
}
|
||||
if store.object(forKey: "MaxImagePreviewSize") != nil {
|
||||
let downloadLimit = store.integer(forKey: "MaxImagePreviewSize");
|
||||
store.removeObject(forKey: "MaxImagePreviewSize");
|
||||
Settings.fileDownloadSizeLimit.setValue(downloadLimit);
|
||||
}
|
||||
store.removeObject(forKey: "new-ui");
|
||||
store.dictionaryRepresentation().forEach { (k, v) in
|
||||
if let key = Settings(rawValue: k) {
|
||||
if isShared(key: key) {
|
||||
|
@ -89,6 +99,29 @@ public enum Settings: String {
|
|||
}
|
||||
}
|
||||
}
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
let removeOlder = Date().addingTimeInterval(7 * 24 * 60 * 60 * (-1.0));
|
||||
for (k,v) in self.sharedDefaults!.dictionaryRepresentation() {
|
||||
if k.starts(with: "upload-") {
|
||||
let hash = k.replacingOccurrences(of: "upload-", with: "");
|
||||
if let timestamp = (v as? [String: Any])?["timestamp"] as? Date {
|
||||
if timestamp < removeOlder {
|
||||
self.sharedDefaults?.removeObject(forKey: k);
|
||||
let localUploadDirUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.siskinim.shared")!.appendingPathComponent("upload", isDirectory: true).appendingPathComponent(hash, isDirectory: false);
|
||||
if FileManager.default.fileExists(atPath: localUploadDirUrl.path) {
|
||||
try? FileManager.default.removeItem(at: localUploadDirUrl);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.sharedDefaults?.removeObject(forKey: k);
|
||||
let localUploadDirUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.siskinim.shared")!.appendingPathComponent("upload", isDirectory: true).appendingPathComponent(hash, isDirectory: false);
|
||||
if FileManager.default.fileExists(atPath: localUploadDirUrl.path) {
|
||||
try? FileManager.default.removeItem(at: localUploadDirUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func setValue(_ value: String?) {
|
||||
|
@ -133,6 +166,10 @@ public enum Settings: String {
|
|||
return Settings.store.integer(forKey: self.rawValue);
|
||||
}
|
||||
|
||||
public func integer() -> Int {
|
||||
return getInt();
|
||||
}
|
||||
|
||||
fileprivate static func valueChanged(forKey key: Settings, oldValue: Any?, newValue: Any?) {
|
||||
var data: [AnyHashable:Any] = ["key": key.rawValue];
|
||||
if oldValue != nil {
|
||||
|
@ -148,7 +185,7 @@ public enum Settings: String {
|
|||
}
|
||||
|
||||
fileprivate static func isShared(key: Settings) -> Bool {
|
||||
return key == Settings.RosterDisplayHiddenGroup || key == Settings.SharingViaHttpUpload;
|
||||
return key == Settings.RosterDisplayHiddenGroup || key == Settings.SharingViaHttpUpload || key == Settings.fileDownloadSizeLimit
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue