Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6408b22
First more or less draft version of background calculations
dmitrysimkin Apr 16, 2025
4ff23a1
Try fix more message types
dmitrysimkin Apr 16, 2025
20c3ddb
Add logs
dmitrysimkin Apr 16, 2025
de354be
try fix reload individual cells
dmitrysimkin Apr 16, 2025
ac31a04
Merge branch 'develop' into chore/offload-conversation-calculation
dmitrysimkin Apr 16, 2025
a16f7ad
Fix accent color crash
dmitrysimkin Apr 16, 2025
bc2d602
Bring back cashing of controllers
dmitrysimkin Apr 16, 2025
2c10e5f
Try fix issues with actions
dmitrysimkin Apr 17, 2025
9e27b8f
Thread safe cache dict and global access accent color + prints
dmitrysimkin Apr 17, 2025
2b91715
Fixed too many updates
dmitrysimkin Apr 17, 2025
7c2e089
Improve sender cell
dmitrysimkin Apr 18, 2025
b9b9e6d
Try fix system message
dmitrysimkin Apr 18, 2025
2e9544e
Fix tests
dmitrysimkin Apr 21, 2025
9b12b59
Code style
dmitrysimkin Apr 21, 2025
b4d5260
Remove prints
dmitrysimkin Apr 21, 2025
5e26e30
Make safer
dmitrysimkin Apr 21, 2025
92e8676
FORMAT
dmitrysimkin Apr 21, 2025
cfee156
Revert atumockable
dmitrysimkin Apr 21, 2025
a17543a
Fixed crash with color
dmitrysimkin Apr 22, 2025
da42bdb
Fix building tests
dmitrysimkin Apr 22, 2025
665835f
Merge branch 'develop' into chore/offload-conversation-calculation
dmitrysimkin Apr 22, 2025
4393a72
Fix crash with unread messages
dmitrysimkin Apr 24, 2025
6564e7f
Improve perforamnce and crashes
dmitrysimkin Apr 25, 2025
914a6d1
Merge branch 'develop' into chore/offload-conversation-calculation
dmitrysimkin Apr 28, 2025
2d78a75
Format
dmitrysimkin Apr 28, 2025
1e52fe6
Re-create view background context
dmitrysimkin Apr 28, 2025
2ba7896
Merge branch 'develop' into chore/offload-conversation-calculation
dmitrysimkin Apr 29, 2025
d628c27
Code style
dmitrysimkin Apr 29, 2025
953e908
improve getting context and for tests
dmitrysimkin Apr 29, 2025
1534aeb
Improve getting main thread object from DB
dmitrysimkin Apr 29, 2025
0bca79e
Address PR comments
dmitrysimkin Apr 29, 2025
563c055
Address more comments
dmitrysimkin Apr 30, 2025
3d089bc
Code style
dmitrysimkin Apr 30, 2025
4844918
Merge branch 'develop' into chore/offload-conversation-calculation
dmitrysimkin May 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// Wire
// Copyright (C) 2025 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

public import Foundation

public final class LeadingTrailingDebouncer<ID: Hashable> {
Comment thread
samwyndham marked this conversation as resolved.
Comment thread
johnxnguyen marked this conversation as resolved.

private struct DebounceState {
var isCooldown = false
var pendingCall: (() -> Void)?
}

private let cooldownTime: TimeInterval
private let queue: DispatchQueue
private var states: [AnyHashable: DebounceState] = [:]

// Unique key for `nil` ID
private let nilKey = UUID()

public init(cooldownTime: TimeInterval, queue: DispatchQueue = .main) {
self.cooldownTime = cooldownTime
self.queue = queue
}

public func call(id: ID?, block: @escaping () -> Void) {

let key: AnyHashable = id.map { AnyHashable($0) } ?? AnyHashable(nilKey)

if states[key] == nil {
states[key] = DebounceState()
}

var state = states[key]!

if !state.isCooldown {
// LEADING: run immediately
block()
state.isCooldown = true

queue.asyncAfter(deadline: .now() + cooldownTime) { [weak self] in

Check warning on line 55 in WireFoundation/Sources/WireFoundation/LeadingTrailingDebouncer.swift

View workflow job for this annotation

GitHub Actions / Test Results

Capture of 'self' with non-sendable type 'LeadingTrailingDebouncer<ID>?' in a `@Sendable` closure

Capture of 'self' with non-sendable type 'LeadingTrailingDebouncer<ID>?' in a `@Sendable` closure
guard let self else { return }

Check warning on line 57 in WireFoundation/Sources/WireFoundation/LeadingTrailingDebouncer.swift

View workflow job for this annotation

GitHub Actions / Test Results

Capture of 'key' with non-sendable type 'AnyHashable' in a `@Sendable` closure

Capture of 'key' with non-sendable type 'AnyHashable' in a `@Sendable` closure
var updatedState = states[key] ?? DebounceState()
updatedState.isCooldown = false

if let trailing = updatedState.pendingCall {
trailing()
updatedState.pendingCall = nil
updatedState.isCooldown = true

queue.asyncAfter(deadline: .now() + cooldownTime) {

Check warning on line 66 in WireFoundation/Sources/WireFoundation/LeadingTrailingDebouncer.swift

View workflow job for this annotation

GitHub Actions / Test Results

Capture of 'self' with non-sendable type 'LeadingTrailingDebouncer<ID>' in a `@Sendable` closure

Capture of 'self' with non-sendable type 'LeadingTrailingDebouncer<ID>' in a `@Sendable` closure

Check warning on line 66 in WireFoundation/Sources/WireFoundation/LeadingTrailingDebouncer.swift

View workflow job for this annotation

GitHub Actions / Test Results

Capture of 'key' with non-sendable type 'AnyHashable' in a `@Sendable` closure

Capture of 'key' with non-sendable type 'AnyHashable' in a `@Sendable` closure
self.states[key]?.isCooldown = false
self.states[key]?.pendingCall = nil
}
}

states[key] = updatedState
}
} else {
// Store for TRAILING
state.pendingCall = block
}

states[key] = state
}
}
57 changes: 57 additions & 0 deletions WireFoundation/Sources/WireFoundation/ThreadSafeDictionary.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// Wire
// Copyright (C) 2025 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import Foundation

public class ThreadSafeDictionary<Key: Hashable, Value> {

public init() {}

private var dictionary = [Key: Value]()
private let queue = DispatchQueue(label: "com.example.dictionaryQueue")

public func set(value: Value?, for key: Key) {
queue.async {

Check warning on line 29 in WireFoundation/Sources/WireFoundation/ThreadSafeDictionary.swift

View workflow job for this annotation

GitHub Actions / Test Results

Capture of 'self' with non-sendable type 'ThreadSafeDictionary<Key, Value>' in a `@Sendable` closure

Capture of 'self' with non-sendable type 'ThreadSafeDictionary<Key, Value>' in a `@Sendable` closure

Check warning on line 29 in WireFoundation/Sources/WireFoundation/ThreadSafeDictionary.swift

View workflow job for this annotation

GitHub Actions / Test Results

Capture of 'key' with non-sendable type 'Key' in a `@Sendable` closure

Capture of 'key' with non-sendable type 'Key' in a `@Sendable` closure

Check warning on line 29 in WireFoundation/Sources/WireFoundation/ThreadSafeDictionary.swift

View workflow job for this annotation

GitHub Actions / Test Results

Capture of 'value' with non-sendable type 'Value?' in a `@Sendable` closure

Capture of 'value' with non-sendable type 'Value?' in a `@Sendable` closure
self.dictionary[key] = value
}
}

public func get(for key: Key) -> Value? {
queue.sync {
self.dictionary[key]
}
}

public func remove(for key: Key) {
queue.async {

Check warning on line 41 in WireFoundation/Sources/WireFoundation/ThreadSafeDictionary.swift

View workflow job for this annotation

GitHub Actions / Test Results

Capture of 'self' with non-sendable type 'ThreadSafeDictionary<Key, Value>' in a `@Sendable` closure

Capture of 'self' with non-sendable type 'ThreadSafeDictionary<Key, Value>' in a `@Sendable` closure

Check warning on line 41 in WireFoundation/Sources/WireFoundation/ThreadSafeDictionary.swift

View workflow job for this annotation

GitHub Actions / Test Results

Capture of 'key' with non-sendable type 'Key' in a `@Sendable` closure

Capture of 'key' with non-sendable type 'Key' in a `@Sendable` closure
self.dictionary.removeValue(forKey: key)
}
}

public func allItems() -> [Key: Value] {
queue.sync {
self.dictionary
}
}

public func reset() {
queue.async {

Check warning on line 53 in WireFoundation/Sources/WireFoundation/ThreadSafeDictionary.swift

View workflow job for this annotation

GitHub Actions / Test Results

Capture of 'self' with non-sendable type 'ThreadSafeDictionary<Key, Value>' in a `@Sendable` closure

Capture of 'self' with non-sendable type 'ThreadSafeDictionary<Key, Value>' in a `@Sendable` closure
self.dictionary.removeAll()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
public let accountContainer: URL
public let applicationContainer: URL

let messagesContainer: PersistentContainer
public let messagesContainer: PersistentContainer
let eventsContainer: PersistentContainer
let dispatchGroup: ZMSDispatchGroup?

Expand Down Expand Up @@ -562,14 +562,14 @@

// MARK: -

class PersistentContainer: NSPersistentContainer {
public class PersistentContainer: NSPersistentContainer {

var storeURL: URL? {
persistentStoreDescriptions.first?.url
}

var storeExists: Bool {
guard let storeURL else {

Check warning on line 572 in wire-ios-data-model/Source/ManagedObjectContext/CoreDataStack.swift

View workflow job for this annotation

GitHub Actions / Test Results

Class 'PersistentContainer' must restate inherited '@unchecked Sendable' conformance

Class 'PersistentContainer' must restate inherited '@unchecked Sendable' conformance
return false
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ public typealias Conversation = ConversationLike & SwiftConversationLike
// sourcery: AutoMockable
@objc
public protocol ConversationLike: AnyObject {

// Any as type eraser to hide NSManagedObjectID behind it
var objectId: Any { get }
Comment thread
johnxnguyen marked this conversation as resolved.

var conversationType: ZMConversationType { get }
var isSelfAnActiveMember: Bool { get }
var teamRemoteIdentifier: UUID? { get }
Expand Down Expand Up @@ -70,6 +74,11 @@ public protocol SwiftConversationLike {
}

extension ZMConversation: ConversationLike {

public var objectId: Any {
objectID
}

public var localParticipantsCount: Int {
localParticipants.count
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,6 @@ public protocol ZMConversationMessage: NSObjectProtocol {
/// The replies quoting this message.
var replies: Set<ZMMessage> { get }

/// An in-memory identifier for tracking the message during its life cycle.
var objectIdentifier: String { get }

/// The links attached to the message.
var linkAttachments: [LinkAttachment]? { get set }

Expand Down Expand Up @@ -245,10 +242,6 @@ extension ZMMessage: ZMConversationMessage {
.sortedAscendingPrependingNil(by: \.serverTimestamp)
}

public var objectIdentifier: String {
nonpersistedObjectIdentifer
}

Comment thread
dmitrysimkin marked this conversation as resolved.
public var causedSecurityLevelDegradation: Bool {
false
}
Expand Down
3 changes: 3 additions & 0 deletions wire-ios-data-model/Source/Model/User/UserType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public protocol UserType: NSObjectProtocol, UserConnections {
/// The identifier which uniquely idenitifies the user in its domain
var remoteIdentifier: UUID! { get }

/// Any as type eraser to hide NSManagedObjectID behind it
var objectId: Any { get }
Comment thread
dmitrysimkin marked this conversation as resolved.

/// The domain which the user originates from
var domain: String? { get }

Expand Down
3 changes: 3 additions & 0 deletions wire-ios-data-model/Source/Model/User/ZMSearchUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ public class ZMSearchUser: NSObject, UserType {
public var teamIdentifier: UUID?
@objc public var user: ZMUser?
public private(set) var hasDownloadedFullUserProfile: Bool = false
public var objectId: Any {
user?.objectId ?? remoteIdentifier!
}

fileprivate weak var contextProvider: ContextProvider?
private let searchUsersCache: SearchUsersCache?
Expand Down
4 changes: 4 additions & 0 deletions wire-ios-data-model/Source/Model/User/ZMUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,10 @@
}
}

var objectId: Any {
objectID
}

/// combination of domain and remoteIdentifier
@NSManaged private var primaryKey: String

Expand Down Expand Up @@ -479,7 +483,7 @@
Set(participantRoles.compactMap(\.conversation))
}
}

Check warning on line 486 in wire-ios-data-model/Source/Model/User/ZMUser.swift

View workflow job for this annotation

GitHub Actions / Test Results

Extension declares a conformance of imported type 'NSManagedObject' to imported protocol 'SafeForLoggingStringConvertible'; this will not behave correctly if the owners of 'CoreData' introduce this conformance in the future

Extension declares a conformance of imported type 'NSManagedObject' to imported protocol 'SafeForLoggingStringConvertible'; this will not behave correctly if the owners of 'CoreData' introduce this conformance in the future
extension NSManagedObject: SafeForLoggingStringConvertible {
public var safeForLoggingDescription: String {
let moc: String = managedObjectContext?.description ?? "nil"
Expand Down
30 changes: 0 additions & 30 deletions wire-ios-data-model/Source/Model/ZMManagedObject.m
Original file line number Diff line number Diff line change
Expand Up @@ -768,33 +768,3 @@ - (BOOL)validateValue:(id *)ioValue forKey:(NSString *)key error:(NSError **)out
}

@end




@implementation ZMManagedObject (NonpersistedObjectIdentifer)

- (NSString *)nonpersistedObjectIdentifer;
{
return [NSString stringWithFormat:@"Z%tx", (unsigned long) self];
}

+ (instancetype)existingObjectWithNonpersistedObjectIdentifer:(NSString *)identifier inUserSession:(id<ContextProvider>)userSession;
{
VerifyReturnNil(identifier != nil);
intptr_t value = 0;
if (sscanf([identifier UTF8String], "Z%tx", &value) != 1) {
return nil;
}

NSManagedObjectContext *moc = userSession.viewContext;
for (ZMManagedObject *mo in moc.registeredObjects) {
intptr_t otherValue = (intptr_t) ((__bridge void *) mo);
if (otherValue == value) {
return mo;
}
}
return nil;
}

@end
8 changes: 0 additions & 8 deletions wire-ios-data-model/Source/Public/ZMManagedObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,3 @@ extern NSString * _Nonnull const ZMDataPropertySuffix;
- (nullable NSString *)objectIDURLString;

@end

@interface ZMManagedObject (NonpersistedObjectIdentifer)

@property (nonatomic, readonly, nonnull) NSString *nonpersistedObjectIdentifer;

+ (nullable instancetype)existingObjectWithNonpersistedObjectIdentifer:(nullable NSString *)identifier inUserSession:(nonnull id<ContextProvider>)userSession;

@end

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -210,15 +210,6 @@ public class MockZMConversationMessage: NSObject, ZMConversationMessage {

public var underlyingReplies: Set<ZMMessage>!

// MARK: - objectIdentifier

public var objectIdentifier: String {
get { return underlyingObjectIdentifier }
set(value) { underlyingObjectIdentifier = value }
}

public var underlyingObjectIdentifier: String!

// MARK: - linkAttachments

public var linkAttachments: [LinkAttachment]?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -692,39 +692,4 @@ extension ZMClientMessageTests_Editing {
XCTAssertEqual(editedMessage.textMessageData?.messageText, editedText)
XCTAssertEqual(editedMessage, newMessage)
}

func testThatMessageNonPersistedIdentifierDoesNotChangeAfterEdit() {
// given
let oldText = "Mamma mia"
let newText = "here we go again"
let oldNonce = UUID.create()

let sender = ZMUser.insertNewObject(in: uiMOC)
sender.remoteIdentifier = UUID.create()

let conversation = ZMConversation.insertNewObject(in: uiMOC)
conversation.remoteIdentifier = UUID.create()
let message: ZMMessage = try! conversation.appendText(content: oldText) as! ZMMessage
message.sender = sender
message.nonce = oldNonce

let oldIdentifier = message.nonpersistedObjectIdentifer
let updateEvent = createMessageEditUpdateEvent(
oldNonce: message.nonce!,
newNonce: UUID.create(),
conversationID: conversation.remoteIdentifier!,
senderID: message.sender!.remoteIdentifier!,
newText: newText
)

// when
var newMessage: ZMClientMessage?
performPretendingUiMocIsSyncMoc {
newMessage = ZMClientMessage.createOrUpdate(from: updateEvent!, in: self.uiMOC, prefetchResult: nil)
}

// then
XCTAssertNotEqual(oldNonce, newMessage!.nonce)
XCTAssertEqual(oldIdentifier, newMessage!.nonpersistedObjectIdentifer)
}
}
Loading
Loading