Updated the SessionShareExtension to work with GRDB
Updated to the latest version of GRDB Fixed an issue with db reentrant behaviour with the Attachment upload function Finished up the updated 'sendNonDurability' functions
This commit is contained in:
parent
8f120c4380
commit
5bcc124388
1
Podfile
1
Podfile
|
@ -42,6 +42,7 @@ abstract_target 'GlobalDependencies' do
|
|||
|
||||
target 'SessionShareExtension' do
|
||||
pod 'NVActivityIndicatorView'
|
||||
pod 'DifferenceKit'
|
||||
end
|
||||
|
||||
target 'SignalUtilitiesKit' do
|
||||
|
|
|
@ -27,7 +27,7 @@ PODS:
|
|||
- DifferenceKit/Core (1.2.0)
|
||||
- DifferenceKit/UIKitExtension (1.2.0):
|
||||
- DifferenceKit/Core
|
||||
- GRDB.swift/SQLCipher (5.23.0):
|
||||
- GRDB.swift/SQLCipher (5.24.0):
|
||||
- SQLCipher (>= 3.4.0)
|
||||
- Mantle (2.1.0):
|
||||
- Mantle/extobjc (= 2.1.0)
|
||||
|
@ -203,7 +203,7 @@ SPEC CHECKSUMS:
|
|||
CryptoSwift: a532e74ed010f8c95f611d00b8bbae42e9fe7c17
|
||||
Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6
|
||||
DifferenceKit: 5659c430bb7fe45876fa32ce5cba5d6167f0c805
|
||||
GRDB.swift: e4a950fe99d113ea5d24571d49eaae0062303c14
|
||||
GRDB.swift: 7ecc8799aaa97cf1fbbcfa9d75821aa920cb713f
|
||||
Mantle: 2fa750afa478cd625a94230fbf1c13462f29395b
|
||||
NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667
|
||||
OpenSSL-Universal: e7311447fd2419f57420c79524b641537387eff2
|
||||
|
@ -219,6 +219,6 @@ SPEC CHECKSUMS:
|
|||
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: 50ae96076a7cd581c63b3276679615844c88ac44
|
||||
PODFILE CHECKSUM: bd0e75b0b6e37b30d8414efed2a5a98635e1a1a6
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
|
|
|
@ -752,6 +752,7 @@
|
|||
FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */; };
|
||||
FD28A4F227E990E800FF65E7 /* BlockingManagerRemovalMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD28A4F127E990E800FF65E7 /* BlockingManagerRemovalMigration.swift */; };
|
||||
FD28A4F427EA79F800FF65E7 /* BlockListUIUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD28A4F327EA79F800FF65E7 /* BlockListUIUtils.swift */; };
|
||||
FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; };
|
||||
FD3C907327E8387300CD579F /* OpenGroupServerIdLookupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907227E8387300CD579F /* OpenGroupServerIdLookupMigration.swift */; };
|
||||
FD3C907527E83AC200CD579F /* OpenGroupServerIdLookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */; };
|
||||
FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; };
|
||||
|
@ -1806,6 +1807,7 @@
|
|||
FD28A4F127E990E800FF65E7 /* BlockingManagerRemovalMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockingManagerRemovalMigration.swift; sourceTree = "<group>"; };
|
||||
FD28A4F327EA79F800FF65E7 /* BlockListUIUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockListUIUtils.swift; sourceTree = "<group>"; };
|
||||
FD28A4F527EAD44C00FF65E7 /* GRDBStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRDBStorage.swift; sourceTree = "<group>"; };
|
||||
FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = "<group>"; };
|
||||
FD3C907227E8387300CD579F /* OpenGroupServerIdLookupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookupMigration.swift; sourceTree = "<group>"; };
|
||||
FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookup.swift; sourceTree = "<group>"; };
|
||||
FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
|
||||
|
@ -2055,6 +2057,7 @@
|
|||
FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */,
|
||||
FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */,
|
||||
C3ADC66026426688005F1414 /* ShareVC.swift */,
|
||||
FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */,
|
||||
B817AD9B26436F73009DF825 /* ThreadPickerVC.swift */,
|
||||
B817AD9926436593009DF825 /* SimplifiedConversationCell.swift */,
|
||||
);
|
||||
|
@ -4546,6 +4549,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */,
|
||||
B817AD9A26436593009DF825 /* SimplifiedConversationCell.swift in Sources */,
|
||||
C3ADC66126426688005F1414 /* ShareVC.swift in Sources */,
|
||||
FD705A90278CEBBC00F16121 /* ShareAppExtensionContext.swift in Sources */,
|
||||
|
|
|
@ -89,7 +89,7 @@ extension ConversationVC:
|
|||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) {
|
||||
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) {
|
||||
sendAttachments(attachments, with: messageText ?? "")
|
||||
resetMentions()
|
||||
self.snInputView.text = ""
|
||||
|
@ -106,7 +106,7 @@ extension ConversationVC:
|
|||
|
||||
// MARK: - AttachmentApprovalViewControllerDelegate
|
||||
|
||||
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) {
|
||||
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) {
|
||||
sendAttachments(attachments, with: messageText ?? "") { [weak self] in
|
||||
self?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
@ -146,9 +146,13 @@ extension ConversationVC:
|
|||
}
|
||||
|
||||
func handleLibraryButtonTapped() {
|
||||
let threadId: String = self.viewModel.viewData.thread.id
|
||||
|
||||
requestLibraryPermissionIfNeeded { [weak self] in
|
||||
DispatchQueue.main.async {
|
||||
let sendMediaNavController = SendMediaNavigationController.showingMediaLibraryFirst()
|
||||
let sendMediaNavController = SendMediaNavigationController.showingMediaLibraryFirst(
|
||||
threadId: threadId
|
||||
)
|
||||
sendMediaNavController.sendMediaNavDelegate = self
|
||||
sendMediaNavController.modalPresentationStyle = .fullScreen
|
||||
self?.present(sendMediaNavController, animated: true, completion: nil)
|
||||
|
@ -165,7 +169,7 @@ extension ConversationVC:
|
|||
SNLog("Proceeding without microphone access. Any recorded video will be silent.")
|
||||
}
|
||||
|
||||
let sendMediaNavController = SendMediaNavigationController.showingCameraFirst()
|
||||
let sendMediaNavController = SendMediaNavigationController.showingCameraFirst(threadId: self.viewModel.viewData.thread.id)
|
||||
sendMediaNavController.sendMediaNavDelegate = self
|
||||
sendMediaNavController.modalPresentationStyle = .fullScreen
|
||||
|
||||
|
@ -234,7 +238,12 @@ extension ConversationVC:
|
|||
}
|
||||
|
||||
func showAttachmentApprovalDialog(for attachments: [SignalAttachment]) {
|
||||
let navController = AttachmentApprovalViewController.wrappedInNavController(attachments: attachments, approvalDelegate: self)
|
||||
let navController = AttachmentApprovalViewController.wrappedInNavController(
|
||||
threadId: self.viewModel.viewData.thread.id,
|
||||
attachments: attachments,
|
||||
approvalDelegate: self
|
||||
)
|
||||
|
||||
present(navController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
@ -505,7 +514,11 @@ extension ConversationVC:
|
|||
let dataSource = DataSourceValue.dataSource(with: imageData, utiType: kUTTypeJPEG as String)
|
||||
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: kUTTypeJPEG as String, imageQuality: .medium)
|
||||
|
||||
let approvalVC = AttachmentApprovalViewController.wrappedInNavController(attachments: [ attachment ], approvalDelegate: self)
|
||||
let approvalVC = AttachmentApprovalViewController.wrappedInNavController(
|
||||
threadId: self.viewModel.viewData.thread.id,
|
||||
attachments: [ attachment ],
|
||||
approvalDelegate: self
|
||||
)
|
||||
approvalVC.modalPresentationStyle = .fullScreen
|
||||
|
||||
self.present(approvalVC, animated: true, completion: nil)
|
||||
|
|
|
@ -15,6 +15,12 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
|
|||
private var dataChangeObservable: DatabaseCancellable?
|
||||
private var hasLoadedInitialData: Bool = false
|
||||
|
||||
// MARK: - Intialization
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
private var tableViewTopConstraint: NSLayoutConstraint!
|
||||
|
@ -205,10 +211,6 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
|
|||
dataChangeObservable?.cancel()
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: - Updating
|
||||
|
||||
private func startObservingChanges() {
|
||||
|
|
|
@ -425,7 +425,11 @@ class NotificationActionHandler {
|
|||
trySendReadReceipt: true
|
||||
)
|
||||
|
||||
return MessageSender.sendNonDurably(db, interaction: interaction, in: thread)
|
||||
return try MessageSender.sendNonDurably(
|
||||
db,
|
||||
interaction: interaction,
|
||||
in: thread
|
||||
)
|
||||
}
|
||||
|
||||
promise.catch { [weak self] error in
|
||||
|
|
|
@ -563,7 +563,7 @@ public extension Attachment {
|
|||
return attachmentsFolder
|
||||
}()
|
||||
|
||||
internal static func originalFilePath(id: String, mimeType: String, sourceFilename: String?) -> String? {
|
||||
public static func originalFilePath(id: String, mimeType: String, sourceFilename: String?) -> String? {
|
||||
return MIMETypeUtil.filePath(
|
||||
forAttachment: id,
|
||||
ofMIMEType: mimeType,
|
||||
|
@ -866,6 +866,7 @@ extension Attachment {
|
|||
|
||||
extension Attachment {
|
||||
internal func upload(
|
||||
_ db: Database,
|
||||
using upload: (Data) -> Promise<UInt64>,
|
||||
encrypt: Bool,
|
||||
success: (() -> Void)?,
|
||||
|
@ -899,11 +900,9 @@ extension Attachment {
|
|||
digest == nil
|
||||
else {
|
||||
// Save the final upload info
|
||||
let uploadedAttachment: Attachment? = GRDBStorage.shared.write { db in
|
||||
try self
|
||||
.with(state: .uploaded)
|
||||
.saved(db)
|
||||
}
|
||||
let uploadedAttachment: Attachment? = try? self
|
||||
.with(state: .uploaded)
|
||||
.saved(db)
|
||||
|
||||
guard uploadedAttachment != nil else {
|
||||
SNLog("Couldn't update attachmentUpload job.")
|
||||
|
@ -943,11 +942,9 @@ extension Attachment {
|
|||
}
|
||||
|
||||
// Update the attachment to the 'uploading' state
|
||||
let updatedAttachment: Attachment? = GRDBStorage.shared.write { db in
|
||||
try processedAttachment
|
||||
.with(state: .uploading)
|
||||
.saved(db)
|
||||
}
|
||||
let updatedAttachment: Attachment? = try? processedAttachment
|
||||
.with(state: .uploading)
|
||||
.saved(db)
|
||||
|
||||
guard updatedAttachment != nil else {
|
||||
SNLog("Couldn't update attachmentUpload job.")
|
||||
|
|
|
@ -404,6 +404,63 @@ public extension Interaction {
|
|||
// MARK: - GRDB Interactions
|
||||
|
||||
public extension Interaction {
|
||||
static func lastInteractionTimestamp(timestampMsKey: String) -> CommonTableExpression<Void> {
|
||||
return CommonTableExpression(
|
||||
named: "lastInteraction",
|
||||
request: Interaction
|
||||
.select(
|
||||
Interaction.Columns.threadId,
|
||||
|
||||
// 'max()' to get the latest
|
||||
max(Interaction.Columns.timestampMs).forKey(timestampMsKey)
|
||||
)
|
||||
.joining(required: Interaction.thread)
|
||||
.group(Interaction.Columns.threadId) // One interaction per thread
|
||||
)
|
||||
}
|
||||
|
||||
static func lastInteraction(
|
||||
lastInteractionKey: String,
|
||||
timestampMsKey: String,
|
||||
threadVariantKey: String,
|
||||
isOpenGroupInvitationKey: String,
|
||||
recipientStatesKey: String
|
||||
) -> CommonTableExpression<Void> {
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
|
||||
|
||||
return CommonTableExpression(
|
||||
named: lastInteractionKey,
|
||||
request: Interaction
|
||||
.select(
|
||||
Interaction.Columns.id,
|
||||
Interaction.Columns.threadId,
|
||||
Interaction.Columns.variant,
|
||||
|
||||
// 'max()' to get the latest
|
||||
max(Interaction.Columns.timestampMs).forKey(timestampMsKey),
|
||||
|
||||
thread[.variant].forKey(threadVariantKey),
|
||||
Interaction.Columns.body,
|
||||
Interaction.Columns.authorId,
|
||||
(linkPreview[.url] != nil).forKey(isOpenGroupInvitationKey)
|
||||
)
|
||||
.joining(required: Interaction.thread.aliased(thread))
|
||||
.joining(
|
||||
optional: Interaction.linkPreview
|
||||
.filter(literal: Interaction.linkPreviewFilterLiteral)
|
||||
.filter(LinkPreview.Columns.variant == LinkPreview.Variant.openGroupInvitation)
|
||||
)
|
||||
.including(all: Interaction.attachments)
|
||||
.including(
|
||||
all: Interaction.recipientStates
|
||||
.select(RecipientState.Columns.state)
|
||||
.forKey(recipientStatesKey)
|
||||
)
|
||||
.group(Interaction.Columns.threadId) // One interaction per thread
|
||||
)
|
||||
}
|
||||
|
||||
/// This will update the `wasRead` state the the interaction
|
||||
///
|
||||
/// - Parameters
|
||||
|
|
|
@ -30,6 +30,16 @@ public struct InteractionAttachment: Codable, FetchableRecord, PersistableRecord
|
|||
request(for: InteractionAttachment.attachment)
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(
|
||||
interactionId: Int64,
|
||||
attachmentId: String
|
||||
) {
|
||||
self.interactionId = interactionId
|
||||
self.attachmentId = attachmentId
|
||||
}
|
||||
|
||||
// MARK: - Custom Database Interaction
|
||||
|
||||
public func delete(_ db: Database) throws -> Bool {
|
||||
|
|
|
@ -33,18 +33,21 @@ public enum AttachmentUploadJob: JobExecutor {
|
|||
return
|
||||
}
|
||||
|
||||
attachment.upload(
|
||||
using: { data in
|
||||
if let openGroup: OpenGroup = openGroup {
|
||||
return OpenGroupAPIV2.upload(data, to: openGroup.room, on: openGroup.server)
|
||||
}
|
||||
|
||||
return FileServerAPIV2.upload(data)
|
||||
},
|
||||
encrypt: (openGroup == nil),
|
||||
success: { success(job, false) },
|
||||
failure: { error in failure(job, error, false) }
|
||||
)
|
||||
GRDBStorage.shared.writeAsync { db in
|
||||
attachment.upload(
|
||||
db,
|
||||
using: { data in
|
||||
if let openGroup: OpenGroup = openGroup {
|
||||
return OpenGroupAPIV2.upload(data, to: openGroup.room, on: openGroup.server)
|
||||
}
|
||||
|
||||
return FileServerAPIV2.upload(data)
|
||||
},
|
||||
encrypt: (openGroup == nil),
|
||||
success: { success(job, false) },
|
||||
failure: { error in failure(job, error, false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,8 +15,8 @@ public final class ClosedGroupControlMessage: ControlMessage {
|
|||
|
||||
public override var ttl: UInt64 {
|
||||
switch kind {
|
||||
case .encryptionKeyPair: return 14 * 24 * 60 * 60 * 1000
|
||||
default: return 14 * 24 * 60 * 60 * 1000
|
||||
case .encryptionKeyPair: return 14 * 24 * 60 * 60 * 1000
|
||||
default: return 14 * 24 * 60 * 60 * 1000
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -184,8 +184,8 @@ public final class ClosedGroupControlMessage: ControlMessage {
|
|||
|
||||
// MARK: - Initialization
|
||||
|
||||
internal init(kind: Kind) {
|
||||
super.init()
|
||||
internal init(kind: Kind, sentTimestampMs: UInt64? = nil) {
|
||||
super.init(sentTimestamp: sentTimestampMs)
|
||||
|
||||
self.kind = kind
|
||||
}
|
||||
|
|
|
@ -73,12 +73,10 @@ extension MessageSender {
|
|||
members: membersAsData,
|
||||
admins: adminsAsData,
|
||||
expirationTimer: 0
|
||||
)
|
||||
)
|
||||
.with(
|
||||
),
|
||||
// Note: We set this here to ensure the value matches the 'ClosedGroup'
|
||||
// object we created
|
||||
sentTimestamp: UInt64(floor(formationTimestamp * 1000))
|
||||
sentTimestampMs: UInt64(floor(formationTimestamp * 1000))
|
||||
),
|
||||
interactionId: nil,
|
||||
in: contactThread
|
||||
|
|
|
@ -6,8 +6,8 @@ import PromiseKit
|
|||
import SessionUtilitiesKit
|
||||
|
||||
extension MessageSender {
|
||||
|
||||
// MARK: Durable
|
||||
|
||||
// MARK: - Durable
|
||||
|
||||
public static func send(_ db: Database, interaction: Interaction, with attachments: [SignalAttachment], in thread: SessionThread) throws {
|
||||
guard let interactionId: Int64 = interaction.id else { throw GRDBStorageError.objectNotSaved }
|
||||
|
@ -61,78 +61,111 @@ extension MessageSender {
|
|||
)
|
||||
}
|
||||
|
||||
// MARK: - Non-Durable
|
||||
|
||||
public static func sendNonDurably(_ db: Database, interaction: Interaction, in thread: SessionThread) -> Promise<Void> {
|
||||
guard let interactionId: Int64 = interaction.id else {
|
||||
return Promise(error: GRDBStorageError.objectNotSaved)
|
||||
}
|
||||
public static func sendNonDurably(_ db: Database, interaction: Interaction, with attachments: [SignalAttachment], in thread: SessionThread) throws -> Promise<Void> {
|
||||
guard let interactionId: Int64 = interaction.id else { return Promise(error: GRDBStorageError.objectNotSaved) }
|
||||
|
||||
let openGroup: OpenGroup? = try? thread.openGroup.fetchOne(db)
|
||||
let attachmentStateInfo: [Attachment.StateInfo] = (try? Attachment
|
||||
.stateInfo(interactionId: interactionId, state: .pending)
|
||||
.fetchAll(db))
|
||||
.defaulting(to: [])
|
||||
let attachmentUploadPromises: [Promise<Void>] = (try? Attachment
|
||||
.filter(ids: attachmentStateInfo.map { $0.attachmentId })
|
||||
.fetchAll(db))
|
||||
.defaulting(to: [])
|
||||
.map { attachment -> Promise<Void> in
|
||||
let (promise, seal) = Promise<Void>.pending()
|
||||
|
||||
attachment.upload(
|
||||
using: { data in
|
||||
if let openGroup: OpenGroup = openGroup {
|
||||
return OpenGroupAPIV2.upload(data, to: openGroup.room, on: openGroup.server)
|
||||
}
|
||||
|
||||
return FileServerAPIV2.upload(data)
|
||||
},
|
||||
encrypt: (openGroup == nil),
|
||||
success: { seal.fulfill(()) },
|
||||
failure: { seal.reject($0) }
|
||||
)
|
||||
|
||||
return promise
|
||||
}
|
||||
try prep(db, signalAttachments: attachments, for: interactionId)
|
||||
|
||||
return when(resolved: attachmentUploadPromises)
|
||||
.then(on: DispatchQueue.global(qos: .userInitiated)) { results -> Promise<Void> in
|
||||
let errors = results
|
||||
.compactMap { result -> Swift.Error? in
|
||||
if case .rejected(let error) = result { return error }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if let error = errors.first { return Promise(error: error) }
|
||||
|
||||
return sendNonDurably(db, interaction: interaction, in: thread)
|
||||
}
|
||||
return sendNonDurably(
|
||||
db,
|
||||
message: VisibleMessage.from(db, interaction: interaction),
|
||||
interactionId: interactionId,
|
||||
to: try Message.Destination.from(db, thread: thread)
|
||||
)
|
||||
}
|
||||
|
||||
public static func sendNonDurably(_ db: Database, _ message: VisibleMessage, with attachmentIds: [String], in thread: TSThread) -> Promise<Void> {
|
||||
|
||||
public static func sendNonDurably(_ db: Database, interaction: Interaction, in thread: SessionThread) throws -> Promise<Void> {
|
||||
// Only 'VisibleMessage' types can be sent via this method
|
||||
guard interaction.variant == .standardOutgoing else { throw MessageSenderError.invalidMessage }
|
||||
guard let interactionId: Int64 = interaction.id else { throw GRDBStorageError.objectNotSaved }
|
||||
|
||||
return sendNonDurably(
|
||||
db,
|
||||
message: VisibleMessage.from(db, interaction: interaction),
|
||||
interactionId: interactionId,
|
||||
to: try Message.Destination.from(db, thread: thread)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
public static func sendNonDurably(_ db: Database, message: Message, interactionId: Int64?, in thread: SessionThread) throws -> Promise<Void> {
|
||||
return try MessageSender.sendImmediate(
|
||||
return sendNonDurably(
|
||||
db,
|
||||
message: message,
|
||||
to: try Message.Destination.from(db, thread: thread),
|
||||
interactionId: interactionId
|
||||
interactionId: interactionId,
|
||||
to: try Message.Destination.from(db, thread: thread)
|
||||
)
|
||||
}
|
||||
|
||||
public static func sendNonDurably(_ message: VisibleMessage, with attachments: [SignalAttachment], in thread: TSThread) -> Promise<Void> {
|
||||
}
|
||||
public static func sendNonDurably(_ db: Database, message: Message, interactionId: Int64?, to destination: Message.Destination) -> Promise<Void> {
|
||||
var attachmentUploadPromises: [Promise<Void>] = [Promise.value(())]
|
||||
|
||||
// If we have an interactionId then check if it has any attachments and process them first
|
||||
if let interactionId: Int64 = interactionId {
|
||||
let threadId: String = {
|
||||
switch destination {
|
||||
case .contact(let publicKey): return publicKey
|
||||
case .closedGroup(let groupPublicKey): return groupPublicKey
|
||||
case .openGroupV2(let room, let server):
|
||||
return OpenGroup.idFor(room: room, server: server)
|
||||
|
||||
case .openGroup: return ""
|
||||
}
|
||||
}()
|
||||
let openGroup: OpenGroup? = try? OpenGroup.fetchOne(db, id: threadId)
|
||||
let attachmentStateInfo: [Attachment.StateInfo] = (try? Attachment
|
||||
.stateInfo(interactionId: interactionId, state: .pending)
|
||||
.fetchAll(db))
|
||||
.defaulting(to: [])
|
||||
|
||||
attachmentUploadPromises = (try? Attachment
|
||||
.filter(ids: attachmentStateInfo.map { $0.attachmentId })
|
||||
.fetchAll(db))
|
||||
.defaulting(to: [])
|
||||
.map { attachment -> Promise<Void> in
|
||||
let (promise, seal) = Promise<Void>.pending()
|
||||
|
||||
public static func sendNonDurably(_ db: Database, message: Message, interactionId: Int64?, to destination: Message.Destination) throws -> Promise<Void> {
|
||||
return try MessageSender.sendImmediate(
|
||||
db,
|
||||
message: message,
|
||||
to: destination,
|
||||
interactionId: interactionId
|
||||
)
|
||||
attachment.upload(
|
||||
db,
|
||||
using: { data in
|
||||
if let openGroup: OpenGroup = openGroup {
|
||||
return OpenGroupAPIV2.upload(data, to: openGroup.room, on: openGroup.server)
|
||||
}
|
||||
|
||||
return FileServerAPIV2.upload(data)
|
||||
},
|
||||
encrypt: (openGroup == nil),
|
||||
success: { seal.fulfill(()) },
|
||||
failure: { seal.reject($0) }
|
||||
)
|
||||
|
||||
return promise
|
||||
}
|
||||
}
|
||||
|
||||
// Once the attachments are processed then send the message
|
||||
return when(resolved: attachmentUploadPromises)
|
||||
.then { results -> Promise<Void> in
|
||||
let errors: [Error] = results
|
||||
.compactMap { result -> Error? in
|
||||
if case .rejected(let error) = result { return error }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if let error: Error = errors.first { return Promise(error: error) }
|
||||
|
||||
return GRDBStorage.shared.write { db in
|
||||
try MessageSender.sendImmediate(
|
||||
db,
|
||||
message: message,
|
||||
to: destination,
|
||||
interactionId: interactionId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This method requires the `db` value to be passed in because if it's called within a `writeAsync` completion block
|
||||
|
|
|
@ -225,6 +225,13 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
|
|||
}
|
||||
|
||||
func shareViewFailed(error: Error) {
|
||||
guard Thread.isMainThread else {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.shareViewFailed(error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let alert = UIAlertController(title: "Session", message: error.localizedDescription, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "BUTTON_OK".localized(), style: .default, handler: { _ in
|
||||
self.extensionContext!.cancelRequest(withError: error)
|
||||
|
|
|
@ -4,9 +4,7 @@ import UIKit
|
|||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
|
||||
final class SimplifiedConversationCell : UITableViewCell {
|
||||
var threadViewModel: ThreadViewModel! { didSet { update() } }
|
||||
|
||||
final class SimplifiedConversationCell: UITableViewCell {
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
|
@ -80,45 +78,25 @@ final class SimplifiedConversationCell : UITableViewCell {
|
|||
accentLineView.set(.width, to: Values.accentLineThickness)
|
||||
accentLineView.set(.height, to: 68)
|
||||
|
||||
let profilePictureViewSize = Values.mediumProfilePictureSize
|
||||
profilePictureView.set(.width, to: profilePictureViewSize)
|
||||
profilePictureView.set(.height, to: profilePictureViewSize)
|
||||
profilePictureView.size = profilePictureViewSize
|
||||
profilePictureView.set(.width, to: Values.mediumProfilePictureSize)
|
||||
profilePictureView.set(.height, to: Values.mediumProfilePictureSize)
|
||||
profilePictureView.size = Values.mediumProfilePictureSize
|
||||
|
||||
stackView.pin(to: self)
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
// MARK: - Updating
|
||||
|
||||
private func update() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let thread = threadViewModel?.thread else { return }
|
||||
|
||||
accentLineView.alpha = (thread.isBlocked() ? 1 : 0)
|
||||
profilePictureView.update(for: thread)
|
||||
displayNameLabel.text = getDisplayName()
|
||||
}
|
||||
|
||||
private func getDisplayName() -> String {
|
||||
if threadViewModel.thread.variant == .closedGroup || threadViewModel.thread.variant == .openGroup {
|
||||
if threadViewModel.name.isEmpty {
|
||||
// TODO: Localization
|
||||
return "Unknown Group"
|
||||
}
|
||||
|
||||
return threadViewModel.name
|
||||
}
|
||||
|
||||
if threadViewModel.threadRecord.isNoteToSelf() {
|
||||
return "NOTE_TO_SELF".localized()
|
||||
}
|
||||
|
||||
guard threadViewModel.thread.variant == .contact else {
|
||||
// TODO: Localization
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
return Profile.displayName(id: threadViewModel.thread.id)
|
||||
public func update(with item: ThreadPickerViewModel.Item, currentUserProfile: Profile) {
|
||||
accentLineView.alpha = (item.isBlocked ? 1 : 0)
|
||||
profilePictureView.update(
|
||||
publicKey: item.id,
|
||||
profile: item.profile(currentUserProfile: currentUserProfile),
|
||||
additionalProfile: item.additionalProfile,
|
||||
threadVariant: item.variant,
|
||||
openGroupProfilePicture: item.openGroupProfilePictureData.map { UIImage(data: $0) },
|
||||
useFallbackPicture: (item.variant == .openGroup && item.openGroupProfilePictureData == nil)
|
||||
)
|
||||
displayNameLabel.text = item.displayName(currentUserProfile: currentUserProfile)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SignalUtilitiesKit
|
||||
import GRDB
|
||||
import PromiseKit
|
||||
import DifferenceKit
|
||||
import SessionUIKit
|
||||
import SignalUtilitiesKit
|
||||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableViewDelegate, AttachmentApprovalViewControllerDelegate {
|
||||
private var threads: YapDatabaseViewMappings!
|
||||
private var threadViewModelCache: [String: ThreadViewModel] = [:] // Thread ID to ThreadViewModel
|
||||
private var selectedThread: TSThread?
|
||||
private let viewModel: ThreadPickerViewModel = ThreadPickerViewModel()
|
||||
private var dataChangeObservable: DatabaseCancellable?
|
||||
private var hasLoadedInitialData: Bool = false
|
||||
|
||||
var shareVC: ShareVC?
|
||||
|
||||
private var threadCount: UInt {
|
||||
threads.numberOfItems(inGroup: TSShareExtensionGroup)
|
||||
}
|
||||
// MARK: - Intialization
|
||||
|
||||
private lazy var dbConnection: YapDatabaseConnection = {
|
||||
let result = OWSPrimaryStorage.shared().newDatabaseConnection()
|
||||
result.objectCacheLimit = 500
|
||||
return result
|
||||
}()
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
|
@ -63,14 +64,6 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
view.backgroundColor = .clear
|
||||
view.setGradient(Gradients.defaultBackground)
|
||||
|
||||
// Threads
|
||||
dbConnection.beginLongLivedReadTransaction() // Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to)
|
||||
threads = YapDatabaseViewMappings(groups: [ TSShareExtensionGroup ], view: TSThreadShareExtensionDatabaseViewExtensionName) // The extension should be registered at this point
|
||||
threads.setIsReversed(true, forGroup: TSShareExtensionGroup)
|
||||
dbConnection.read { transaction in
|
||||
self.threads.update(with: transaction) // Perform the initial update
|
||||
}
|
||||
|
||||
// Title
|
||||
navigationItem.titleView = titleLabel
|
||||
|
||||
|
@ -80,8 +73,41 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
view.addSubview(fadeView)
|
||||
|
||||
setupLayout()
|
||||
// Reload
|
||||
reload()
|
||||
|
||||
// Notifications
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(applicationDidBecomeActive(_:)),
|
||||
name: UIApplication.didBecomeActiveNotification,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(applicationDidResignActive(_:)),
|
||||
name: UIApplication.didEnterBackgroundNotification, object: nil
|
||||
)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
startObservingChanges()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
// Stop observing database changes
|
||||
dataChangeObservable?.cancel()
|
||||
}
|
||||
|
||||
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
||||
startObservingChanges()
|
||||
}
|
||||
|
||||
@objc func applicationDidResignActive(_ notification: Notification) {
|
||||
// Stop observing database changes
|
||||
dataChangeObservable?.cancel()
|
||||
}
|
||||
|
||||
private func setupNavBar() {
|
||||
|
@ -112,55 +138,83 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
fadeView.pin(.bottom, to: .bottom, of: view)
|
||||
}
|
||||
|
||||
// MARK: Table View Data Source
|
||||
// MARK: - Updating
|
||||
|
||||
private func startObservingChanges() {
|
||||
// Start observing for data changes
|
||||
dataChangeObservable = GRDBStorage.shared.start(
|
||||
viewModel.observableViewData,
|
||||
onError: { _ in },
|
||||
onChange: { [weak self] viewData in
|
||||
// The defaul scheduler emits changes on the main thread
|
||||
self?.handleUpdates(viewData)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func handleUpdates(_ updatedViewData: ThreadPickerViewModel.ViewData) {
|
||||
// Ensure the first load runs without animations (if we don't do this the cells will animate
|
||||
// in from a frame of CGRect.zero)
|
||||
guard hasLoadedInitialData else {
|
||||
hasLoadedInitialData = true
|
||||
UIView.performWithoutAnimation { handleUpdates(updatedViewData) }
|
||||
return
|
||||
}
|
||||
|
||||
// Reload the table content (animate changes after the first load)
|
||||
tableView.reload(
|
||||
using: StagedChangeset(source: viewModel.viewData.items, target: updatedViewData.items),
|
||||
with: .automatic,
|
||||
interrupt: { $0.changeCount > 100 } // Prevent too many changes from causing performance issues
|
||||
) { [weak self] updatedData in
|
||||
self?.viewModel.updateData(
|
||||
ThreadPickerViewModel.ViewData(
|
||||
currentUserProfile: updatedViewData.currentUserProfile,
|
||||
items: updatedData
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return Int(threadCount)
|
||||
return self.viewModel.viewData.items.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: SimplifiedConversationCell = tableView.dequeue(type: SimplifiedConversationCell.self, for: indexPath)
|
||||
cell.threadViewModel = threadViewModel(at: indexPath.row)
|
||||
cell.update(
|
||||
with: self.viewModel.viewData.items[indexPath.row],
|
||||
currentUserProfile: self.viewModel.viewData.currentUserProfile
|
||||
)
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
// MARK: - Updating
|
||||
|
||||
private func reload() {
|
||||
AssertIsOnMainThread()
|
||||
dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit
|
||||
dbConnection.read { transaction in
|
||||
self.threads.update(with: transaction)
|
||||
}
|
||||
threadViewModelCache.removeAll()
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
||||
guard let thread = self.thread(at: indexPath.row), let attachments = ShareVC.attachmentPrepPromise?.value else {
|
||||
return
|
||||
}
|
||||
guard let attachments: [SignalAttachment] = ShareVC.attachmentPrepPromise?.value else { return }
|
||||
|
||||
self.selectedThread = thread
|
||||
|
||||
let approvalVC = AttachmentApprovalViewController.wrappedInNavController(attachments: attachments, approvalDelegate: self)
|
||||
navigationController!.present(approvalVC, animated: true, completion: nil)
|
||||
let approvalVC: OWSNavigationController = AttachmentApprovalViewController.wrappedInNavController(
|
||||
threadId: self.viewModel.viewData.items[indexPath.row].id,
|
||||
attachments: attachments,
|
||||
approvalDelegate: self
|
||||
)
|
||||
self.navigationController?.present(approvalVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) {
|
||||
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) {
|
||||
// Sharing a URL or plain text will populate the 'messageText' field so in those
|
||||
// cases we should ignore the attachments
|
||||
let isSharingUrl: Bool = (attachments.count == 1 && attachments[0].isUrl)
|
||||
let isSharingText: Bool = (attachments.count == 1 && attachments[0].isText)
|
||||
let finalAttachments: [SignalAttachment] = (isSharingUrl || isSharingText ? [] : attachments)
|
||||
|
||||
let message = VisibleMessage()
|
||||
message.sentTimestamp = NSDate.millisecondTimestamp()
|
||||
message.text = (isSharingUrl && (messageText?.isEmpty == true || attachments[0].linkPreviewDraft == nil) ?
|
||||
let body: String? = (
|
||||
isSharingUrl && (messageText?.isEmpty == true || attachments[0].linkPreviewDraft == nil) ?
|
||||
(
|
||||
(messageText?.isEmpty == true || (attachments[0].text() == messageText) ?
|
||||
attachments[0].text() :
|
||||
|
@ -169,35 +223,55 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
) :
|
||||
messageText
|
||||
)
|
||||
|
||||
let tsMessage = TSOutgoingMessage.from(message, associatedWith: selectedThread!)
|
||||
Storage.write(
|
||||
with: { transaction in
|
||||
if isSharingUrl {
|
||||
message.linkPreview = VisibleMessage.LinkPreview.from(
|
||||
attachments[0].linkPreviewDraft,
|
||||
using: transaction
|
||||
)
|
||||
}
|
||||
else {
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
},
|
||||
completion: {
|
||||
if isSharingUrl {
|
||||
tsMessage.linkPreview = OWSLinkPreview.from(message.linkPreview)
|
||||
|
||||
Storage.write { transaction in
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
shareVC!.dismiss(animated: true, completion: nil)
|
||||
shareVC?.dismiss(animated: true, completion: nil)
|
||||
|
||||
ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in
|
||||
MessageSender.sendNonDurably(message, with: finalAttachments, in: self.selectedThread!)
|
||||
GRDBStorage.shared
|
||||
.write { [weak self] db -> Promise<Void> in
|
||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
|
||||
activityIndicator.dismiss { }
|
||||
self?.shareVC?.shareViewFailed(error: MessageSenderError.noThread)
|
||||
return Promise(error: MessageSenderError.noThread)
|
||||
}
|
||||
|
||||
// Create the interaction
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let interaction: Interaction = try Interaction(
|
||||
threadId: threadId,
|
||||
authorId: getUserHexEncodedPublicKey(db),
|
||||
variant: .standardOutgoing,
|
||||
body: body,
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)),
|
||||
hasMention: (body?.contains("@\(userPublicKey)") == true),
|
||||
linkPreviewUrl: (isSharingUrl ? attachments.first?.linkPreviewDraft?.urlString : nil)
|
||||
).inserted(db)
|
||||
|
||||
// If the user is sharing a Url, there is a LinkPreview and it doesn't match an existing
|
||||
// one then add it now
|
||||
if
|
||||
isSharingUrl,
|
||||
let linkPreviewDraft: OWSLinkPreviewDraft = attachments.first?.linkPreviewDraft,
|
||||
(try? interaction.linkPreview.isEmpty(db)) == true
|
||||
{
|
||||
try LinkPreview(
|
||||
url: linkPreviewDraft.urlString,
|
||||
title: linkPreviewDraft.title,
|
||||
attachmentId: LinkPreview.saveAttachmentIfPossible(
|
||||
db,
|
||||
imageData: linkPreviewDraft.jpegImageData,
|
||||
mimeType: OWSMimeTypeImageJpeg
|
||||
)
|
||||
).insert(db)
|
||||
}
|
||||
|
||||
return try MessageSender.sendNonDurably(
|
||||
db,
|
||||
interaction: interaction,
|
||||
with: finalAttachments,
|
||||
in: thread
|
||||
)
|
||||
}
|
||||
.done { [weak self] _ in
|
||||
activityIndicator.dismiss { }
|
||||
self?.shareVC?.shareViewWasCompleted()
|
||||
|
@ -216,31 +290,4 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeMessageText newMessageText: String?) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
private func thread(at index: Int) -> TSThread? {
|
||||
var thread: TSThread? = nil
|
||||
dbConnection.read { transaction in
|
||||
let ext = transaction.ext(TSThreadShareExtensionDatabaseViewExtensionName) as! YapDatabaseViewTransaction
|
||||
thread = ext.object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread?
|
||||
}
|
||||
return thread
|
||||
}
|
||||
|
||||
private func threadViewModel(at index: Int) -> ThreadViewModel? {
|
||||
guard let thread = thread(at: index) else { return nil }
|
||||
|
||||
if let cachedThreadViewModel = threadViewModelCache[thread.uniqueId!] {
|
||||
return cachedThreadViewModel
|
||||
}
|
||||
else {
|
||||
var threadViewModel: ThreadViewModel? = nil
|
||||
dbConnection.read { transaction in
|
||||
threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
|
||||
}
|
||||
threadViewModelCache[thread.uniqueId!] = threadViewModel
|
||||
return threadViewModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import DifferenceKit
|
||||
import SignalUtilitiesKit
|
||||
import SessionMessagingKit
|
||||
|
||||
public class ThreadPickerViewModel {
|
||||
// MARK: - Initialization
|
||||
|
||||
init() {
|
||||
viewData = ViewData(
|
||||
currentUserProfile: Profile.fetchOrCreateCurrentUser(),
|
||||
items: []
|
||||
)
|
||||
}
|
||||
|
||||
public struct Item: FetchableRecord, Decodable, Equatable, Differentiable {
|
||||
public struct GroupMemberInfo: FetchableRecord, Decodable, Equatable {
|
||||
public let profile: Profile
|
||||
}
|
||||
|
||||
fileprivate static let closedGroupNameKey = CodingKeys.closedGroupName.stringValue
|
||||
fileprivate static let openGroupNameKey = CodingKeys.openGroupName.stringValue
|
||||
fileprivate static let openGroupProfilePictureDataKey = CodingKeys.openGroupProfilePictureData.stringValue
|
||||
fileprivate static let contactProfileKey = CodingKeys.contactProfile.stringValue
|
||||
fileprivate static let closedGroupAvatarProfilesKey = CodingKeys.closedGroupAvatarProfiles.stringValue
|
||||
fileprivate static let contactIsBlockedKey = CodingKeys.contactIsBlocked.stringValue
|
||||
fileprivate static let isNoteToSelfKey = CodingKeys.isNoteToSelf.stringValue
|
||||
|
||||
public var differenceIdentifier: String { id }
|
||||
|
||||
public let id: String
|
||||
public let variant: SessionThread.Variant
|
||||
|
||||
public let closedGroupName: String?
|
||||
public let openGroupName: String?
|
||||
public let openGroupProfilePictureData: Data?
|
||||
private let contactProfile: Profile?
|
||||
private let closedGroupAvatarProfiles: [GroupMemberInfo]?
|
||||
|
||||
/// A flag indicating whether the contact is blocked (will be null for non-contact threads)
|
||||
private let contactIsBlocked: Bool?
|
||||
public let isNoteToSelf: Bool
|
||||
|
||||
public func displayName(currentUserProfile: Profile) -> String {
|
||||
return SessionThread.displayName(
|
||||
threadId: id,
|
||||
variant: variant,
|
||||
closedGroupName: closedGroupName,
|
||||
openGroupName: openGroupName,
|
||||
isNoteToSelf: isNoteToSelf,
|
||||
profile: contactProfile
|
||||
)
|
||||
}
|
||||
|
||||
public func profile(currentUserProfile: Profile) -> Profile? {
|
||||
switch variant {
|
||||
case .contact: return contactProfile
|
||||
case .openGroup: return nil
|
||||
case .closedGroup:
|
||||
// If there is only a single user in the group then we want to use the current user
|
||||
// profile at the back
|
||||
if closedGroupAvatarProfiles?.count == 1 {
|
||||
return currentUserProfile
|
||||
}
|
||||
|
||||
return closedGroupAvatarProfiles?.first?.profile
|
||||
}
|
||||
}
|
||||
|
||||
public var additionalProfile: Profile? {
|
||||
switch variant {
|
||||
case .closedGroup: return closedGroupAvatarProfiles?.last?.profile
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// A flag indicating whether the thread is blocked (only contact threads can be blocked)
|
||||
public var isBlocked: Bool {
|
||||
return (contactIsBlocked == true)
|
||||
}
|
||||
|
||||
// MARK: - Query
|
||||
|
||||
public static func query(userPublicKey: String) -> QueryInterfaceRequest<Item> {
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
let contact: TypedTableAlias<Contact> = TypedTableAlias()
|
||||
let closedGroup: TypedTableAlias<ClosedGroup> = TypedTableAlias()
|
||||
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
|
||||
let lastInteraction: TableAlias = TableAlias()
|
||||
|
||||
let lastInteractionTimestampExpression: CommonTableExpression = Interaction.lastInteractionTimestamp(
|
||||
timestampMsKey: Interaction.Columns.timestampMs.stringValue
|
||||
)
|
||||
// FIXME: Exclude unwritable opengroups
|
||||
return SessionThread
|
||||
.select(
|
||||
thread[.id],
|
||||
thread[.variant],
|
||||
thread[.creationDateTimestamp],
|
||||
|
||||
closedGroup[.name].forKey(Item.closedGroupNameKey),
|
||||
openGroup[.name].forKey(Item.openGroupNameKey),
|
||||
openGroup[.imageData].forKey(Item.openGroupProfilePictureDataKey),
|
||||
|
||||
contact[.isBlocked].forKey(Item.contactIsBlockedKey),
|
||||
SessionThread.isNoteToSelf(userPublicKey: userPublicKey).forKey(Item.isNoteToSelfKey)
|
||||
)
|
||||
.filter(SessionThread.Columns.shouldBeVisible == true)
|
||||
.filter(SessionThread.isNotMessageRequest(userPublicKey: userPublicKey))
|
||||
.filter(
|
||||
// Only show the Note to Self if it has an interaction
|
||||
SessionThread.Columns.id != userPublicKey ||
|
||||
lastInteraction[Interaction.Columns.timestampMs] != nil
|
||||
)
|
||||
.aliased(thread)
|
||||
.joining(
|
||||
optional: SessionThread.contact
|
||||
.aliased(contact)
|
||||
.including(
|
||||
optional: Contact.profile
|
||||
.forKey(Item.contactProfileKey)
|
||||
)
|
||||
)
|
||||
.joining(
|
||||
optional: SessionThread.closedGroup
|
||||
.aliased(closedGroup)
|
||||
.including(
|
||||
all: ClosedGroup.members
|
||||
.filter(GroupMember.Columns.role == GroupMember.Role.standard)
|
||||
.filter(GroupMember.Columns.profileId != userPublicKey)
|
||||
.order(GroupMember.Columns.profileId) // Sort to provide a level of stability
|
||||
.limit(2)
|
||||
.including(required: GroupMember.profile)
|
||||
.forKey(Item.closedGroupAvatarProfilesKey)
|
||||
)
|
||||
)
|
||||
.joining(optional: SessionThread.openGroup.aliased(openGroup))
|
||||
.with(lastInteractionTimestampExpression)
|
||||
.including(
|
||||
optional: SessionThread
|
||||
.association(
|
||||
to: lastInteractionTimestampExpression,
|
||||
on: { thread, lastInteraction in
|
||||
thread[SessionThread.Columns.id] == lastInteraction[Interaction.Columns.threadId]
|
||||
}
|
||||
)
|
||||
.aliased(lastInteraction)
|
||||
)
|
||||
.order(
|
||||
(
|
||||
lastInteraction[Interaction.Columns.timestampMs] ??
|
||||
(thread[.creationDateTimestamp] * 1000)
|
||||
).desc
|
||||
)
|
||||
.asRequest(of: Item.self)
|
||||
}
|
||||
}
|
||||
|
||||
public struct ViewData: Equatable {
|
||||
let currentUserProfile: Profile
|
||||
let items: [Item]
|
||||
}
|
||||
|
||||
/// This value is the current state of the view
|
||||
public private(set) var viewData: ViewData
|
||||
|
||||
/// This is all the data the screen needs to populate itself, please see the following link for tips to help optimise
|
||||
/// performance https://github.com/groue/GRDB.swift#valueobservation-performance
|
||||
///
|
||||
/// **Note:** The 'trackingConstantRegion' is optimised in such a way that the request needs to be static
|
||||
/// otherwise there may be situations where it doesn't get updates, this means we can't have conditional queries
|
||||
public lazy var observableViewData = ValueObservation
|
||||
.trackingConstantRegion { db -> ViewData in
|
||||
let currentUserProfile: Profile = Profile.fetchOrCreateCurrentUser(db)
|
||||
return ViewData(
|
||||
currentUserProfile: Profile.fetchOrCreateCurrentUser(db),
|
||||
items: try Item
|
||||
.query(userPublicKey: currentUserProfile.id)
|
||||
.fetchAll(db)
|
||||
)
|
||||
}
|
||||
.removeDuplicates()
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
public func updateData(_ updatedData: ViewData) {
|
||||
self.viewData = updatedData
|
||||
}
|
||||
}
|
|
@ -503,7 +503,7 @@ public final class JobRunner {
|
|||
runNextJob()
|
||||
}
|
||||
return
|
||||
|
||||
|
||||
// For "blocking once per session" jobs only rerun it immediately if it hasn't already
|
||||
// run this session
|
||||
case .recurringOnLaunchBlockingOncePerSession:
|
||||
|
@ -517,7 +517,7 @@ public final class JobRunner {
|
|||
runNextJob()
|
||||
}
|
||||
return
|
||||
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
|
@ -531,6 +531,8 @@ public final class JobRunner {
|
|||
maxFailureCount >= 0 &&
|
||||
job.failureCount + 1 < maxFailureCount
|
||||
else {
|
||||
SNLog("[JobRunner] \(job.variant) failed permanently\(maxFailureCount >= 0 ? "; too many retries" : "")")
|
||||
|
||||
// If the job permanently failed or we have performed all of our retry attempts
|
||||
// then delete the job (it'll probably never succeed)
|
||||
_ = try job.delete(db)
|
||||
|
|
|
@ -14,6 +14,7 @@ public protocol AttachmentApprovalViewControllerDelegate: AnyObject {
|
|||
func attachmentApproval(
|
||||
_ attachmentApproval: AttachmentApprovalViewController,
|
||||
didApproveAttachments attachments: [SignalAttachment],
|
||||
forThreadId threadId: String,
|
||||
messageText: String?
|
||||
)
|
||||
|
||||
|
@ -54,6 +55,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
|
|||
// MARK: - Properties
|
||||
|
||||
private let mode: Mode
|
||||
private let threadId: String
|
||||
private let isAddMoreVisible: Bool
|
||||
|
||||
public weak var approvalDelegate: AttachmentApprovalViewControllerDelegate?
|
||||
|
@ -123,10 +125,12 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
|
|||
@objc
|
||||
required public init(
|
||||
mode: Mode,
|
||||
threadId: String,
|
||||
attachments: [SignalAttachment]
|
||||
) {
|
||||
assert(attachments.count > 0)
|
||||
self.mode = mode
|
||||
self.threadId = threadId
|
||||
let attachmentItems = attachments.map { SignalAttachmentItem(attachment: $0 )}
|
||||
self.isAddMoreVisible = (mode == .sharedNavigation)
|
||||
|
||||
|
@ -154,12 +158,12 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
|
|||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func wrappedInNavController(
|
||||
threadId: String,
|
||||
attachments: [SignalAttachment],
|
||||
approvalDelegate: AttachmentApprovalViewControllerDelegate
|
||||
) -> OWSNavigationController {
|
||||
let vc = AttachmentApprovalViewController(mode: .modal, attachments: attachments)
|
||||
let vc = AttachmentApprovalViewController(mode: .modal, threadId: threadId, attachments: attachments)
|
||||
vc.approvalDelegate = approvalDelegate
|
||||
|
||||
let navController = OWSNavigationController(rootViewController: vc)
|
||||
|
@ -760,7 +764,7 @@ extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate {
|
|||
attachmentTextToolbar.isUserInteractionEnabled = false
|
||||
attachmentTextToolbar.isHidden = true
|
||||
|
||||
approvalDelegate?.attachmentApproval(self, didApproveAttachments: attachments, messageText: attachmentTextToolbar.messageText)
|
||||
approvalDelegate?.attachmentApproval(self, didApproveAttachments: attachments, forThreadId: threadId, messageText: attachmentTextToolbar.messageText)
|
||||
}
|
||||
|
||||
func attachmentTextToolbarDidChange(_ attachmentTextToolbar: AttachmentTextToolbar) {
|
||||
|
|
|
@ -63,8 +63,13 @@ public class ModalActivityIndicatorViewController: OWSViewController {
|
|||
}
|
||||
|
||||
@objc
|
||||
public func dismiss(completion : @escaping () -> Void) {
|
||||
AssertIsOnMainThread()
|
||||
public func dismiss(completion: @escaping () -> Void) {
|
||||
guard Thread.isMainThread else {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.dismiss(completion: completion)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !wasDimissed {
|
||||
// Only dismiss once.
|
||||
|
|
|
@ -46,7 +46,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
TSAccountManager *tsAccountManager = [[TSAccountManager alloc] initWithPrimaryStorage:primaryStorage];
|
||||
id<SSKReachabilityManager> reachabilityManager = [SSKReachabilityManagerImpl new];
|
||||
id<OWSTypingIndicators> typingIndicators = [[OWSTypingIndicatorsImpl alloc] init];
|
||||
|
||||
OWSAudioSession *audioSession = [OWSAudioSession new];
|
||||
id<OWSProximityMonitoringManager> proximityMonitoringManager = [OWSProximityMonitoringManagerImpl new];
|
||||
|
@ -61,8 +60,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
// TODO: Refactor this file to Swift
|
||||
[SSKEnvironment setShared:[[SSKEnvironment alloc] initWithPrimaryStorage:primaryStorage
|
||||
tsAccountManager:tsAccountManager
|
||||
reachabilityManager:reachabilityManager
|
||||
typingIndicators:typingIndicators]];
|
||||
reachabilityManager:reachabilityManager]];
|
||||
// [SSKEnvironment setShared:[[SSKEnvironment alloc] initWithPrimaryStorage:primaryStorage
|
||||
// tsAccountManager:tsAccountManager
|
||||
// disappearingMessagesJob:disappearingMessagesJob
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#import "UIView+OWS.h"
|
||||
#import "UIViewController+OWS.h"
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SessionUIKit/SessionUIKit.h>
|
||||
|
||||
#import <SessionUtilitiesKit/AppContext.h>
|
||||
|
||||
|
@ -83,7 +84,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
const CGFloat kExtraRightPadding = isRTL ? -0 : +10;
|
||||
|
||||
// Extra hit area above/below
|
||||
const CGFloat kExtraHeightPadding = 4;
|
||||
const CGFloat kExtraHeightPadding = 8;
|
||||
|
||||
// Matching the default backbutton placement is tricky.
|
||||
// We can't just adjust the imageEdgeInsets on a UIBarButtonItem directly,
|
||||
|
@ -91,39 +92,19 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
// in a UIBarButtonItem.
|
||||
[backButton addTarget:target action:selector forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
UIImage *backImage = [[UIImage imageNamed:(isRTL ? @"NavBarBackRTL" : @"NavBarBack")]
|
||||
imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
UIImageConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:22 weight:UIImageSymbolWeightMedium];
|
||||
UIImage *backImage = [[UIImage systemImageNamed:@"chevron.backward" withConfiguration:config] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
OWSAssertDebug(backImage);
|
||||
[backButton setImage:backImage forState:UIControlStateNormal];
|
||||
backButton.tintColor = UIColor.lokiGreen;
|
||||
backButton.tintColor = LKColors.text;
|
||||
|
||||
backButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
|
||||
backButton.imageEdgeInsets = UIEdgeInsetsMake(0, kExtraLeftPadding, 0, 0);
|
||||
|
||||
// Default back button is 1.5 pixel lower than our extracted image.
|
||||
const CGFloat kTopInsetPadding = 1.5;
|
||||
backButton.imageEdgeInsets = UIEdgeInsetsMake(kTopInsetPadding, kExtraLeftPadding, 0, 0);
|
||||
|
||||
CGRect buttonFrame
|
||||
= CGRectMake(0, 0, backImage.size.width + kExtraRightPadding, backImage.size.height + kExtraHeightPadding);
|
||||
CGRect buttonFrame = CGRectMake(0, 0, backImage.size.width + kExtraRightPadding, backImage.size.height + kExtraHeightPadding);
|
||||
backButton.frame = buttonFrame;
|
||||
|
||||
// In iOS 11.1 beta, the hot area of custom bar button items is _only_
|
||||
// the bounds of the custom view, making them very hard to hit.
|
||||
//
|
||||
// TODO: Remove this hack if the bug is fixed in iOS 11.1 by the time
|
||||
// it goes to production (or in a later release),
|
||||
// since it has two negative side effects: 1) the layout of the
|
||||
// back button isn't consistent with the iOS default back buttons
|
||||
// 2) we can't add the unread count badge to the back button
|
||||
// with this hack.
|
||||
return [[UIBarButtonItem alloc] initWithImage:backImage
|
||||
style:UIBarButtonItemStylePlain
|
||||
target:target
|
||||
action:selector];
|
||||
|
||||
UIBarButtonItem *backItem =
|
||||
[[UIBarButtonItem alloc] initWithCustomView:backButton
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"back")];
|
||||
UIBarButtonItem *backItem = [[UIBarButtonItem alloc] initWithCustomView:backButton accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"back")];
|
||||
backItem.width = buttonFrame.size.width;
|
||||
|
||||
return backItem;
|
||||
|
|
Loading…
Reference in New Issue