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:
Morgan Pretty 2022-05-15 14:39:21 +10:00
parent 8f120c4380
commit 5bcc124388
22 changed files with 619 additions and 283 deletions

View File

@ -42,6 +42,7 @@ abstract_target 'GlobalDependencies' do
target 'SessionShareExtension' do
pod 'NVActivityIndicatorView'
pod 'DifferenceKit'
end
target 'SignalUtilitiesKit' do

View File

@ -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

View File

@ -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 */,

View File

@ -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)

View File

@ -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() {

View File

@ -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

View File

@ -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.")

View File

@ -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

View File

@ -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 {

View File

@ -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) }
)
}
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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) {

View File

@ -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.

View File

@ -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

View File

@ -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;