-
Notifications
You must be signed in to change notification settings - Fork 24
feat: improve swipe pt.1 - WPB-16787 #3019
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/base-improve-swipe-WPB-16787
Are you sure you want to change the base?
Changes from all commits
11063f1
978c8f7
ae06404
169f6be
2b2b268
ff4ec51
dfd6845
18524f6
5c0cae1
4548f6c
d17b475
28f8fba
8a29cff
d698036
2a4b3ea
e8d7ae7
cabd2a9
54d41ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
@@ -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
|
||
/// 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) | ||
} | ||
|
||
@Published var state: State | ||
|
||
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 | ||
result.append(imageAttr) | ||
} | ||
} | ||
|
||
// Team role icons | ||
switch teamRoleIndicator { // TODO: addd resources | ||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will rename in next PRs |
||||||
|
||||||
var body: some View { | ||||||
switch model.state { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||
) | ||||||
} | ||||||
} | ||||||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
?There was a problem hiding this comment.
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
forMessageStatusViewModel
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 andcallList
, 4 in total. So approach is the same, @jullianm hope that makes more clear now?