Fixed a number of bugs

Fixed a bug where threads might not be getting marked as read correctly
Fixed a bug where the GarbageCollectionJob could end up blocking the database write thread (seemed to only hang when the debugger was attached but may have affected devices at some point)
Fixed a bug with thread sorting
Fixed a bug where joining an open group wouldn't appear until after the first poll completed
Fixed a bug where conversations with no interactions would display odd interaction copy
Fixed a bug where the sender name was appearing above outgoing messages in groups
pull/612/head
Morgan Pretty 3 years ago
parent 20dc74bc96
commit 2cd9f571da

@ -381,6 +381,9 @@ public final class FullConversationCell: UITableViewCell {
// MARK: - Snippet generation // MARK: - Snippet generation
private func getSnippet(cellViewModel: SessionThreadViewModel) -> NSMutableAttributedString { private func getSnippet(cellViewModel: SessionThreadViewModel) -> NSMutableAttributedString {
// If we don't have an interaction then do nothing
guard cellViewModel.interactionId != nil else { return NSMutableAttributedString() }
let result = NSMutableAttributedString() let result = NSMutableAttributedString()
if Date().timeIntervalSince1970 < (cellViewModel.threadMutedUntilTimestamp ?? 0) { if Date().timeIntervalSince1970 < (cellViewModel.threadMutedUntilTimestamp ?? 0) {

@ -462,7 +462,24 @@ public extension Interaction {
} }
// If we aren't including older interactions then update and save the current one // If we aren't including older interactions then update and save the current one
guard includingOlder else { struct InteractionReadInfo: Decodable, FetchableRecord {
let timestampMs: Int64
let wasRead: Bool
}
// Since there is no guarantee on the order messages are inserted into the database
// fetch the timestamp for the interaction and set everything before that as read
let maybeInteractionInfo: InteractionReadInfo? = try Interaction
.select(.timestampMs, .wasRead)
.filter(id: interactionId)
.asRequest(of: InteractionReadInfo.self)
.fetchOne(db)
guard includingOlder, let interactionInfo: InteractionReadInfo = maybeInteractionInfo else {
// Only mark as read and trigger the subsequent jobs if the interaction is
// actually not read (no point updating and triggering db changes otherwise)
guard maybeInteractionInfo?.wasRead == false else { return }
_ = try Interaction _ = try Interaction
.filter(id: interactionId) .filter(id: interactionId)
.updateAll(db, Columns.wasRead.set(to: true)) .updateAll(db, Columns.wasRead.set(to: true))
@ -472,9 +489,9 @@ public extension Interaction {
} }
let interactionQuery = Interaction let interactionQuery = Interaction
.filter(Columns.threadId == threadId) .filter(Interaction.Columns.threadId == threadId)
.filter(Columns.id <= interactionId) .filter(Interaction.Columns.timestampMs <= interactionInfo.timestampMs)
.filter(Columns.wasRead == false) .filter(Interaction.Columns.wasRead == false)
// The `wasRead` flag doesn't apply to `standardOutgoing` or `standardIncomingDeleted` // The `wasRead` flag doesn't apply to `standardOutgoing` or `standardIncomingDeleted`
.filter(Columns.variant != Variant.standardOutgoing && Columns.variant != Variant.standardIncomingDeleted) .filter(Columns.variant != Variant.standardOutgoing && Columns.variant != Variant.standardIncomingDeleted)
let interactionIdsToMarkAsRead: [Int64] = try interactionQuery let interactionIdsToMarkAsRead: [Int64] = try interactionQuery

@ -35,8 +35,6 @@ public enum GarbageCollectionJob: JobExecutor {
} }
let timestampNow: TimeInterval = Date().timeIntervalSince1970 let timestampNow: TimeInterval = Date().timeIntervalSince1970
var attachmentLocalRelativePaths: Set<String> = []
var profileAvatarFilenames: Set<String> = []
GRDBStorage.shared.writeAsync( GRDBStorage.shared.writeAsync(
updates: { db in updates: { db in
@ -203,6 +201,19 @@ public enum GarbageCollectionJob: JobExecutor {
) )
""") """)
} }
},
completion: { _, _ in
// Dispatch async so we can swap from the write queue to a read one (we are done writing)
queue.async {
// Retrieve a list of all valid attachmnet and avatar file paths
struct FileInfo {
let attachmentLocalRelativePaths: Set<String>
let profileAvatarFilenames: Set<String>
}
let maybeFileInfo: FileInfo? = GRDBStorage.shared.read { db -> FileInfo in
var attachmentLocalRelativePaths: Set<String> = []
var profileAvatarFilenames: Set<String> = []
/// Orphaned attachment files - attachment files which don't have an associated record in the database /// Orphaned attachment files - attachment files which don't have an associated record in the database
if details.typesToCollect.contains(.orphanedAttachmentFiles) { if details.typesToCollect.contains(.orphanedAttachmentFiles) {
@ -225,12 +236,16 @@ public enum GarbageCollectionJob: JobExecutor {
.asRequest(of: String.self) .asRequest(of: String.self)
.fetchSet(db) .fetchSet(db)
} }
},
completion: { _, result in return FileInfo(
// If any of the above failed then we don't want to continue (we would end up deleting all files since attachmentLocalRelativePaths: attachmentLocalRelativePaths,
// neither of the arrays would have been populated correctly) profileAvatarFilenames: profileAvatarFilenames
guard case .success = result else { )
SNLog("[GarbageCollectionJob] Database queries failed, skipping file cleanup") }
// If we couldn't get the file lists then fail (invalid state and don't want to delete all attachment/profile files)
guard let fileInfo: FileInfo = maybeFileInfo else {
failure(job, StorageError.generic, false)
return return
} }
@ -261,7 +276,7 @@ public enum GarbageCollectionJob: JobExecutor {
.filter { path -> Bool in path.contains("/") } .filter { path -> Bool in path.contains("/") }
.compactMap { path -> String? in path.components(separatedBy: "/").first } .compactMap { path -> String? in path.components(separatedBy: "/").first }
let orphanedAttachmentFiles: Set<String> = allAttachmentFilePaths let orphanedAttachmentFiles: Set<String> = allAttachmentFilePaths
.subtracting(attachmentLocalRelativePaths) .subtracting(fileInfo.attachmentLocalRelativePaths)
.subtracting(directoryNamesContainingContent) .subtracting(directoryNamesContainingContent)
orphanedAttachmentFiles.forEach { filepath in orphanedAttachmentFiles.forEach { filepath in
@ -285,7 +300,7 @@ public enum GarbageCollectionJob: JobExecutor {
.defaulting(to: []) .defaulting(to: [])
.asSet() .asSet()
let orphanedAvatarFiles: Set<String> = allAvatarProfileFilenames let orphanedAvatarFiles: Set<String> = allAvatarProfileFilenames
.subtracting(profileAvatarFilenames) .subtracting(fileInfo.profileAvatarFilenames)
orphanedAvatarFiles.forEach { filename in orphanedAvatarFiles.forEach { filename in
// We don't want a single deletion failure to block deletion of the other files so try // We don't want a single deletion failure to block deletion of the other files so try
@ -307,6 +322,7 @@ public enum GarbageCollectionJob: JobExecutor {
success(job, false) success(job, false)
} }
}
) )
} }
} }

@ -169,6 +169,7 @@ public final class OpenGroupManager: NSObject {
// Optionally try to insert a new version of the OpenGroup (it will fail if there is already an // Optionally try to insert a new version of the OpenGroup (it will fail if there is already an
// inactive one but that won't matter as we then activate it // inactive one but that won't matter as we then activate it
_ = try? SessionThread.fetchOrCreate(db, id: threadId, variant: .openGroup) _ = try? SessionThread.fetchOrCreate(db, id: threadId, variant: .openGroup)
_ = try? SessionThread.filter(id: threadId).updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true))
if (try? OpenGroup.exists(db, id: threadId)) == false { if (try? OpenGroup.exists(db, id: threadId)) == false {
try? OpenGroup try? OpenGroup

@ -101,6 +101,8 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
public let authorName: String public let authorName: String
/// This value will be used to populate the author label, if it's null then the label will be hidden /// This value will be used to populate the author label, if it's null then the label will be hidden
///
/// **Note:** This will only be populated for incoming messages
public let senderName: String? public let senderName: String?
/// A flag indicating whether the profile view should be displayed /// A flag indicating whether the profile view should be displayed
@ -331,6 +333,11 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
return nil return nil
} }
// Only show for incoming messages
guard self.variant == .standardIncoming || self.variant == .standardIncomingDeleted else {
return nil
}
// Only if there is a date header or the senders are different // Only if there is a date header or the senders are different
guard shouldShowDateOnThisModel || self.authorId != prevModel?.authorId else { guard shouldShowDateOnThisModel || self.authorId != prevModel?.authorId else {
return nil return nil

@ -500,13 +500,13 @@ public extension SessionThreadViewModel {
static let homeOrderSQL: SQL = { static let homeOrderSQL: SQL = {
let thread: TypedTableAlias<SessionThread> = TypedTableAlias() let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
return SQL("\(thread[.isPinned]) DESC, IFNULL(\(Interaction.self).\(ViewModel.interactionTimestampMsKey), \(thread[.creationDateTimestamp])) DESC") return SQL("\(thread[.isPinned]) DESC, IFNULL(\(Interaction.self).\(ViewModel.interactionTimestampMsKey), (\(thread[.creationDateTimestamp]) * 1000)) DESC")
}() }()
static let messageRequetsOrderSQL: SQL = { static let messageRequetsOrderSQL: SQL = {
let thread: TypedTableAlias<SessionThread> = TypedTableAlias() let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
return SQL("IFNULL(\(Interaction.self).\(ViewModel.interactionTimestampMsKey), \(thread[.creationDateTimestamp])) DESC") return SQL("IFNULL(\(Interaction.self).\(ViewModel.interactionTimestampMsKey), (\(thread[.creationDateTimestamp]) * 1000)) DESC")
}() }()
} }
@ -1388,7 +1388,7 @@ public extension SessionThreadViewModel {
) )
GROUP BY \(thread[.id]) GROUP BY \(thread[.id])
ORDER BY IFNULL(\(interaction[.timestampMs]), \(thread[.creationDateTimestamp])) DESC ORDER BY IFNULL(\(interaction[.timestampMs]), (\(thread[.creationDateTimestamp]) * 1000)) DESC
""" """
return request.adapted { db in return request.adapted { db in

Loading…
Cancel
Save