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
3 changes: 2 additions & 1 deletion WireUI/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ let package = Package(

.target(
name: "WireConversationUI",
dependencies: ["WireAccountImageUI", "WireDesign", "WireFoundation", "WireReusableUIComponents"]
dependencies: ["WireAccountImageUI", "WireDesign", "WireFoundation", "WireReusableUIComponents"],
plugins: [.plugin(name: "SwiftGenPlugin", package: "WirePlugins")]
),
.testTarget(name: "WireConversationUITests", dependencies: ["WireConversationUI"]),

Expand Down
14 changes: 14 additions & 0 deletions WireUI/Sources/WireConversationUI/.swiftgen.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Every input/output paths in the rest of the config will then be expressed relative to these.

input_dir: ./
output_dir: ${GENERATED}/

# Generate constants for your localized strings.

strings:
inputs:
- Resources/Localization/en.lproj/Localizable.strings
filter:
outputs:
- templateName: structured-swift5
output: Strings+Generated.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//

public enum ConversationCellModel: Hashable, Sendable {
import SwiftUI

public enum ConversationCellModel {

/// Used to group messages by time.
case timeDivider(TimeDividerModel)
/// Text Message
case text(TextMessageViewModel)

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
//
import Foundation

// Wire
// Copyright (C) 2025 Wire Swiss GmbH
//
Expand All @@ -15,17 +17,11 @@
// 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 SwiftUI

import UIKit

public extension ConversationCellModel {

var cellReuseIdentifier: String {
switch self {

case .timeDivider:
"timeDivider"
}
public struct AvatarViewModel: Hashable, Sendable {
let color: Color
public init(color: Color) {
self.color = color
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//
// 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 Combine
import SwiftUI
import WireDesign

public protocol SenderObserverProtocol {
var authorChangedPublisher: AnyPublisher<String, Never> { get }
}

public enum TeamRoleIndicator {
case guest
case externalPartner
case federated
case service
}

public class MessageSenderViewModelWrapper: ObservableObject {

Check warning on line 35 in WireUI/Sources/WireConversationUI/ConversationCell/CellTypes/TextMessage/Message Sender/MessageSenderViewModel.swift

View workflow job for this annotation

GitHub Actions / SwiftFormat

Remove trailing space at end of a line. (trailingSpace)

Check failure on line 35 in WireUI/Sources/WireConversationUI/ConversationCell/CellTypes/TextMessage/Message Sender/MessageSenderViewModel.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Lines should not have trailing whitespace (trailing_whitespace)
/// State needed here to be able to update view
/// because it's not possible to have optional 'MessageSenderViewModel?'
/// and @Published together
public enum State {
case none
case some(MessageSenderViewModel)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Can't we get rid of the wrapper and use the view model directly as @ObservedObject by removing the constraint of having it optional ? The view should always be tightly coupled to its view model so no need for an optional here, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I got the suggestion right, can you pls elaborate @jullianm ?
For some cases we don't need to show sender so we need some way to tell that, nil or some specific state

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a view model wrapper , can't we use a state inside the view model like for the MessageStatusViewModel ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I have case none for MessageStatusViewModel as well for a case when we don't need to show status. It's just happen that status view has 2 more states: fail and callList, 4 in total. So approach is the same, @jullianm hope that makes more clear now?


Check failure on line 43 in WireUI/Sources/WireConversationUI/ConversationCell/CellTypes/TextMessage/Message Sender/MessageSenderViewModel.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Lines should not have trailing whitespace (trailing_whitespace)
@Published var state: State

Check failure on line 45 in WireUI/Sources/WireConversationUI/ConversationCell/CellTypes/TextMessage/Message Sender/MessageSenderViewModel.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Lines should not have trailing whitespace (trailing_whitespace)
public init(state: State) {
self.state = state
}
}

public class MessageSenderViewModel: ObservableObject, Identifiable {

public let id = UUID()

let avatarViewModel: AvatarViewModel
private var senderModel: UserModel
let isDeleted: Bool
let teamRoleIndicator: TeamRoleIndicator?
@Published var senderAttributed: AttributedString

private let authorChanged: any SenderObserverProtocol
private var cancellables: Set<AnyCancellable> = []

public init(
avatarViewModel: AvatarViewModel,
senderModel: UserModel,
isDeleted: Bool,
teamRoleIndicator: TeamRoleIndicator?,
authorChanged: any SenderObserverProtocol
) {
self.avatarViewModel = avatarViewModel
self.authorChanged = authorChanged
self.senderModel = senderModel
self.isDeleted = isDeleted
self.teamRoleIndicator = teamRoleIndicator
self.senderAttributed = Self.makeSenderAttributed(
senderModel: senderModel,
isDeleted: isDeleted,
teamRoleIndicator: teamRoleIndicator
)

observeChanges()
}

private func observeChanges() {
authorChanged.authorChangedPublisher
.receive(on: RunLoop.main)
.sink { [weak self] senderString in
guard let self else { return }
print("DS: author ChangedPublisher \(senderString)")
senderModel.name = senderString
senderAttributed = Self.makeSenderAttributed(
senderModel: senderModel,
isDeleted: isDeleted,
teamRoleIndicator: teamRoleIndicator
)
}.store(in: &cancellables)
}

private static func makeSenderAttributed(
senderModel: UserModel,
isDeleted: Bool,
teamRoleIndicator: TeamRoleIndicator?
) -> AttributedString {

let textColor: UIColor = senderModel.isServiceUser ? SemanticColors.Label.textDefault : senderModel.accentColor

var result = AttributedString(senderModel.name ?? L10n.Name.unavailable)
result.foregroundColor = Color(textColor)
result.font = Font(UIFont.mediumSemiboldFont)

// Paragraph style (max line height) // TODO
// let lineHeight = UIFont.mediumSemiboldFont.lineHeight
// let paragraph = NSMutableParagraphStyle()
// paragraph.maximumLineHeight = lineHeight
// result.paragraphStyle = paragraph

// Attachments (convert UIImage to NSTextAttachment and then NSAttributedString, then embed in AttributedString)
func imageAttachment(_ name: String, size: CGFloat) -> AttributedString? {
guard let image = UIImage(named: name) else { return nil }
let attachment = NSTextAttachment()
attachment.image = image
attachment.bounds = CGRect(
x: 0,
y: (UIFont.mediumSemiboldFont.capHeight - size).rounded() / 2,
width: size,
height: size
)
return AttributedString(NSAttributedString(attachment: attachment))
}

// Indicator icon
if isDeleted {
if let imageAttr = imageAttachment("trash", size: 8) { // TODO: addd resources

Check failure on line 134 in WireUI/Sources/WireConversationUI/ConversationCell/CellTypes/TextMessage/Message Sender/MessageSenderViewModel.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Add JIRA reference to TODOs and FIX MEs like [WPB-680]. (todo_requires_jira_link)
result.append(imageAttr)
}
}

// Team role icons
switch teamRoleIndicator { // TODO: addd resources

Check failure on line 140 in WireUI/Sources/WireConversationUI/ConversationCell/CellTypes/TextMessage/Message Sender/MessageSenderViewModel.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Add JIRA reference to TODOs and FIX MEs like [WPB-680]. (todo_requires_jira_link)
case .guest:
if let imageAttr = imageAttachment("guest", size: 14) {
result.append(imageAttr)
}
case .externalPartner:
if let imageAttr = imageAttachment("externalPartner", size: 16) {
result.append(imageAttr)
}
case .federated:
if let imageAttr = imageAttachment("federated", size: 14) {
result.append(imageAttr)
}
case .service:
if let imageAttr = imageAttachment("bot", size: 14) {
result.append(imageAttr)
}
default:
break
}

return result

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// 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 SwiftUI

struct SenderMessageView: View {

@ObservedObject var model: MessageSenderViewModelWrapper

var body: some View {
switch model.state {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: I think here instead of relying on the view model state itself (whether it's optional or not) the view model should always be tightly coupled to its view and provide some state inside that the view would check (i.e model.state..)

case .none:
EmptyView() // nothing when don't need to show sender
case .some(let model):

Check warning on line 29 in WireUI/Sources/WireConversationUI/ConversationCell/CellTypes/TextMessage/Message Sender/SenderMessageView.swift

View workflow job for this annotation

GitHub Actions / SwiftFormat

Reposition let or var bindings within pattern. (hoistPatternLet)
Text(model.senderAttributed)
.frame(maxWidth: .infinity, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
.animation(.easeInOut, value: model.senderAttributed)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// 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 SwiftUI

struct MessageStatusView: View {

@ObservedObject var model: MessageStatusViewModel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@ObservedObject var model: MessageStatusViewModel
@ObservedObject var viewModel: MessageStatusViewModel

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will rename in next PRs


var body: some View {
switch model.state {
Copy link
Contributor

@jullianm jullianm May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep like this using the view model state property directly 👍

case .none:
EmptyView() // when no need to show status view
case let .sendFailure(_):
EmptyView() // will be implemented later
case let .callList(_):
EmptyView() // will be implemented later
case let .details(statusDetails):
MessageToolboxView(
detailsText: statusDetails.timestamp,
editedString: statusDetails.editedString,
deliveryStatus: statusDetails.deliveryState
)
}
}
}
Loading
Loading