Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -798,10 +798,13 @@ public final class ClientSessionComponent {

// MARK: - Other

public let callReactionSubject = PassthroughSubject<CallReactionEvent, Never>()

public private(set) lazy var conversationProtobufMessageProcessor = ConversationProtobufMessageProcessor(
messageLocalStore: messageLocalStore,
conversationLocalStore: conversationLocalStore,
userLocalStore: userLocalStore
userLocalStore: userLocalStore,
onCallReaction: { [callReactionSubject] event in callReactionSubject.send(event) }
)

public private(set) lazy var oneOnOneResolver = OneOnOneResolver(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,18 @@ public struct ConversationProtobufMessageProcessor: ConversationProtobufMessageP
let messageLocalStore: any MessageLocalStoreProtocol
let conversationLocalStore: any ConversationLocalStoreProtocol
let userLocalStore: any UserLocalStoreProtocol
let onCallReaction: ((CallReactionEvent) -> Void)?

public init(
messageLocalStore: any MessageLocalStoreProtocol,
conversationLocalStore: any ConversationLocalStoreProtocol,
userLocalStore: any UserLocalStoreProtocol
userLocalStore: any UserLocalStoreProtocol,
onCallReaction: ((CallReactionEvent) -> Void)? = nil
) {
self.messageLocalStore = messageLocalStore
self.conversationLocalStore = conversationLocalStore
self.userLocalStore = userLocalStore
self.onCallReaction = onCallReaction
}

public func processProtobufMessage(
Expand Down Expand Up @@ -182,10 +185,15 @@ public struct ConversationProtobufMessageProcessor: ConversationProtobufMessageP
// case not handled here, see `onProcessedCallEvent`
break

case .inCallEmoji:
case let .inCallEmoji(inCallEmoji):

// Not supported yet, just discard. TODO: [WPB-11770] implement here
break
// TODO: [WPB-11770] add unit tests for inCallEmoji processing
let sender = await userLocalStore.fetchOrCreateUser(id: senderID.id, domain: senderID.domain)
let senderName = await userLocalStore.name(for: sender) ?? ""
for (emoji, _) in inCallEmoji.emojis {
WireLogger.calling.debug("Received in-call emoji reaction: \(emoji)", attributes: logAttributes)
onCallReaction?(CallReactionEvent(emoji: emoji, senderID: senderID.id, senderName: senderName, conversationID: conversationID.id))
}

case .image, .asset:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// Wire
// Copyright (C) 2026 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 struct CallReactionEvent: Sendable {
public let emoji: String
public let senderID: UUID
public let senderName: String
public let conversationID: UUID

public init(emoji: String, senderID: UUID, senderName: String, conversationID: UUID) {
self.emoji = emoji
self.senderID = senderID
self.senderName = senderName
self.conversationID = conversationID
}
}
1 change: 1 addition & 0 deletions WireUI/Sources/WireLocators/Locators.swift
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ public enum Locators {

case endOngoingCallButton = "End call"
case timeLabel
case callReactionButton

public static func participantIdentifier(_ name: String) -> String {
"audioView.\(name).minimized.inactive"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ public extension GenericMessage {
$0.clientAction = action
}
}

init(emoji: String, count: Int32 = 1) {
self = GenericMessage.with {
$0.inCallEmoji = .with {
$0.emojis = [emoji: count]
}
}
}

}

public extension GenericMessage {
Expand Down
4 changes: 3 additions & 1 deletion wire-ios-sync-engine/Source/Calling/CallKitManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,9 @@ public class CallKitManager: NSObject, CallKitManagerInterface {
if let error {
self?.logger.error("fail: report incoming call: \(error)", attributes: .safePublic)
self?.callRegister.unregisterCall(call)
conversation.voiceChannel?.leave()
conversation.managedObjectContext?.perform {
conversation.voiceChannel?.leave()
}
} else {
self?.logger.info("success: report incoming call", attributes: .safePublic)
self?.mediaManager?.setupAudioDevice()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class StrategyDirectory: NSObject, StrategyDirectoryProtocol {
public private(set) var requestStrategies: [RequestStrategy]
public private(set) var contextChangeTrackers: [ZMContextChangeTracker]
public private(set) var clientContextChangeTrackers: [ZMContextChangeTracker] = []
public private(set) var messageSender: MessageSenderInterface?

init(
contextProvider: ContextProvider,
Expand Down Expand Up @@ -332,6 +333,7 @@ public class StrategyDirectory: NSObject, StrategyDirectoryProtocol {
featureRepository: LegacyFeatureRepository(context: syncContext),
apiVersion: metadata.apiVersion
)
self.messageSender = messageSender

let strategies: [Any] = [
AssetClientMessageRequestStrategy(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// Wire
// Copyright (C) 2026 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 CoreData
import GenericMessageProtocol
import WireDataModel
import WireRequestStrategy

public protocol SendCallReactionUseCaseProtocol {
func invoke(emoji: String, in conversation: ZMConversation) async throws
}

public struct SendCallReactionUseCase: SendCallReactionUseCaseProtocol, Sendable {

let messageSender: MessageSenderInterface
let context: NSManagedObjectContext

public init(messageSender: MessageSenderInterface, context: NSManagedObjectContext) {
self.messageSender = messageSender
self.context = context
}

public func invoke(emoji: String, in conversation: ZMConversation) async throws {
let managedObjectID = conversation.objectID
let syncConversation = try await context.perform {
try context.existingObject(with: managedObjectID) as? ZMConversation
}
let content = InCallEmoji.with { $0.emojis = [emoji: 1] }
let entity = GenericMessageEntity(
message: GenericMessage(content: content),
context: context,
conversation: syncConversation,
completionHandler: nil
)
try await messageSender.sendMessage(message: entity)
}
}
2 changes: 2 additions & 0 deletions wire-ios-sync-engine/Source/UserSession/UserSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ public protocol UserSession: AnyObject {

func makeSearchUsersUseCase() -> SearchUsersUseCaseProtocol?

func makeSendCallReactionUseCase() -> SendCallReactionUseCaseProtocol?

func fetchSelfConversationMLSGroupID() async -> MLSGroupID?

func resolveOneOnOneConversation(with userID: WireDataModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,13 @@ extension ZMUserSession: UserSession {
)
}

public func makeSendCallReactionUseCase() -> SendCallReactionUseCaseProtocol? {
guard let messageSender = (strategyDirectory as? StrategyDirectory)?.messageSender else {
return nil
}
return SendCallReactionUseCase(messageSender: messageSender, context: syncContext)
}

public var resolvedBackendMetadata: BackendMetadataProvider {
let metadata = userSessionComponent.backendMetadata
return BackendMetadataProvider(
Expand Down
4 changes: 4 additions & 0 deletions wire-ios-utilities/Source/DeveloperFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public enum DeveloperFlag: String, CaseIterable {
// TODO: [WPB-25941] Remove drive permissions flag when feature is complete
case enableDrivePermissions
case unSafeLogsForPublic
case inCallReactions

public var description: String {
switch self {
Expand Down Expand Up @@ -117,6 +118,9 @@ public enum DeveloperFlag: String, CaseIterable {

case .unSafeLogsForPublic:
"Turn on to write all logs (including debug and non-public) to disk in release builds"

case .inCallReactions:
"Turn on to enable in-call emoji reactions"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ class VoiceChannelStreamArrangementTests: XCTestCase {

// MARK: - Video streams sorting

// TODO: Replace with a test for sortByPriority (screenshare → active speaker → rest) once sortByVideo is removed
func testThatItReturnsSortedParticipantsInGrid_ByVideoState() {
// GIVEN
let participant1 = participantStub(for: mockUser1, videoEnabled: true)
Expand Down
4 changes: 4 additions & 0 deletions wire-ios/Wire-iOS/Generated/Strings+Generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6561,6 +6561,10 @@ internal enum L10n {
internal static let title = L10n.tr("Localizable", "voice.call_error.unsupported_version.title", fallback: "Please update Wire")
}
}
internal enum CallReactionButton {
/// Emoji
internal static let title = L10n.tr("Localizable", "voice.call_reaction_button.title", fallback: "Emoji")
}
internal enum Calling {
/// Calling...
internal static let title = L10n.tr("Localizable", "voice.calling.title", fallback: "Calling...")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,7 @@
"voice.video_button.title" = "Camera";
"voice.flip_video_button.title" = "Switch camera";
"voice.speaker_button.title" = "Speaker";
"voice.call_reaction_button.title" = "Emoji";
"voice.cancel_button.title" = "Cancel";
"voice.call_button.title" = "Call";
"voice.end_call_button.title" = "End Call";
Expand Down
Loading
Loading