mirror of
https://gitlab.linphone.org/BC/public/linphone-iphone.git
synced 2026-01-17 11:08:06 +00:00
Added the floating button to the UIList (UIKit list)
This commit is contained in:
parent
be414f3c14
commit
923c290fa0
3 changed files with 178 additions and 139 deletions
|
|
@ -164,58 +164,14 @@ struct ConversationFragment: View {
|
|||
|
||||
if #available(iOS 16.0, *) {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
UIList(viewModel: viewModel,
|
||||
paginationState: paginationState,
|
||||
conversationViewModel: conversationViewModel,
|
||||
conversationsListViewModel: conversationsListViewModel,
|
||||
geometryProxy: geometry,
|
||||
sections: conversationViewModel.conversationMessagesSection
|
||||
UIList(
|
||||
viewModel: viewModel,
|
||||
paginationState: paginationState,
|
||||
conversationViewModel: conversationViewModel,
|
||||
conversationsListViewModel: conversationsListViewModel,
|
||||
geometryProxy: geometry,
|
||||
sections: conversationViewModel.conversationMessagesSection
|
||||
)
|
||||
|
||||
if !conversationViewModel.isScrolledToBottom {
|
||||
Button {
|
||||
NotificationCenter.default.post(name: .onScrollToBottom, object: nil)
|
||||
} label: {
|
||||
ZStack {
|
||||
|
||||
Image("caret-down")
|
||||
.renderingMode(.template)
|
||||
.foregroundStyle(.white)
|
||||
.padding()
|
||||
.background(Color.orangeMain500)
|
||||
.clipShape(Circle())
|
||||
.shadow(color: .black.opacity(0.2), radius: 4)
|
||||
|
||||
if conversationViewModel.displayedConversationUnreadMessagesCount > 0 {
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
Text(
|
||||
conversationViewModel.displayedConversationUnreadMessagesCount < 99
|
||||
? String(conversationViewModel.displayedConversationUnreadMessagesCount)
|
||||
: "99+"
|
||||
)
|
||||
.foregroundStyle(.white)
|
||||
.default_text_style(styleSize: 10)
|
||||
.lineLimit(1)
|
||||
|
||||
}
|
||||
.frame(width: 18, height: 18)
|
||||
.background(Color.redDanger500)
|
||||
.cornerRadius(50)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.frame(width: 50, height: 50)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
conversationViewModel.getMessages()
|
||||
|
|
|
|||
|
|
@ -26,6 +26,64 @@ public extension Notification.Name {
|
|||
static let onScrollToIndex = Notification.Name("onScrollToIndex")
|
||||
}
|
||||
|
||||
class FloatingButton: UIButton {
|
||||
|
||||
var unreadMessageCount: Int = 0 {
|
||||
didSet {
|
||||
updateUnreadBadge()
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupButton()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupButton()
|
||||
}
|
||||
|
||||
private func setupButton() {
|
||||
// Set the button's appearance
|
||||
self.setImage(UIImage(named: "caret-down")?.withRenderingMode(.alwaysTemplate), for: .normal)
|
||||
self.tintColor = .white
|
||||
self.backgroundColor = UIColor(Color.orangeMain500)
|
||||
self.layer.cornerRadius = 30
|
||||
self.layer.shadowColor = UIColor.black.withAlphaComponent(0.2).cgColor
|
||||
self.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
self.layer.shadowOpacity = 1
|
||||
self.layer.shadowRadius = 4
|
||||
|
||||
// Add target action
|
||||
self.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
|
||||
}
|
||||
|
||||
private func updateUnreadBadge() {
|
||||
// Remove old badge if exists
|
||||
self.viewWithTag(100)?.removeFromSuperview()
|
||||
|
||||
if unreadMessageCount > 0 {
|
||||
// Create the badge view
|
||||
let badgeLabel = UILabel()
|
||||
badgeLabel.tag = 100
|
||||
badgeLabel.text = unreadMessageCount < 99 ? "\(unreadMessageCount)" : "99+"
|
||||
badgeLabel.textColor = .white
|
||||
badgeLabel.font = UIFont.systemFont(ofSize: 10)
|
||||
badgeLabel.textAlignment = .center
|
||||
badgeLabel.backgroundColor = UIColor(Color.redDanger500)
|
||||
badgeLabel.layer.cornerRadius = 9
|
||||
badgeLabel.layer.masksToBounds = true
|
||||
badgeLabel.frame = CGRect(x: self.frame.size.width - 18, y: 0, width: 18, height: 18)
|
||||
self.addSubview(badgeLabel)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func buttonTapped() {
|
||||
NotificationCenter.default.post(name: .onScrollToBottom, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct UIList: UIViewRepresentable {
|
||||
|
||||
private static var sharedCoordinator: Coordinator?
|
||||
|
|
@ -41,7 +99,11 @@ struct UIList: UIViewRepresentable {
|
|||
@State private var isScrolledToTop = false
|
||||
@State private var isScrolledToBottom = true
|
||||
|
||||
func makeUIView(context: Context) -> UITableView {
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
// Create a UIView to contain the UITableView and UIButton
|
||||
let containerView = UIView()
|
||||
|
||||
// Create the UITableView
|
||||
let tableView = UITableView(frame: .zero, style: .grouped)
|
||||
tableView.contentInset = UIEdgeInsets(top: -10, left: 0, bottom: 0, right: 0)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
|
@ -57,49 +119,81 @@ struct UIList: UIViewRepresentable {
|
|||
tableView.backgroundColor = UIColor(.white)
|
||||
tableView.scrollsToTop = true
|
||||
|
||||
// Create the floating UIButton
|
||||
let button = FloatingButton(frame: CGRect(x: 0, y: 0, width: 60, height: 60))
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.isHidden = isScrolledToBottom
|
||||
|
||||
// Add the tableView and floating button to the containerView
|
||||
containerView.addSubview(tableView)
|
||||
containerView.addSubview(button)
|
||||
|
||||
// Set up constraints
|
||||
NSLayoutConstraint.activate([
|
||||
// TableView constraints
|
||||
tableView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||
tableView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
|
||||
// Floating Button constraints
|
||||
button.widthAnchor.constraint(equalToConstant: 60),
|
||||
button.heightAnchor.constraint(equalToConstant: 60),
|
||||
button.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),
|
||||
button.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -20)
|
||||
])
|
||||
|
||||
// Set the tableView as a tag for easy access in updateUIView
|
||||
tableView.tag = 101
|
||||
// Set the button as a tag for easy access in updateUIView
|
||||
button.tag = 102
|
||||
|
||||
context.coordinator.parent = self
|
||||
context.coordinator.tableView = tableView
|
||||
context.coordinator.floatingButton = button
|
||||
context.coordinator.geometryProxy = geometryProxy
|
||||
|
||||
DispatchQueue.main.async {
|
||||
conversationViewModel.isScrolledToBottom = true
|
||||
}
|
||||
|
||||
return tableView
|
||||
|
||||
return containerView
|
||||
}
|
||||
|
||||
func updateUIView(_ tableView: UITableView, context: Context) {
|
||||
if context.coordinator.sections == sections {
|
||||
return
|
||||
}
|
||||
if context.coordinator.sections == sections {
|
||||
return
|
||||
//func updateUIView(_ tableView: UITableView, context: Context) {
|
||||
func updateUIView(_ uiView: UIView, context: Context) {
|
||||
if let button = uiView.viewWithTag(102) as? FloatingButton {
|
||||
button.unreadMessageCount = conversationViewModel.displayedConversationUnreadMessagesCount
|
||||
}
|
||||
|
||||
let prevSections = context.coordinator.sections
|
||||
let (appliedDeletes, appliedDeletesSwapsAndEdits, deleteOperations, swapOperations, editOperations, insertOperations) = operationsSplit(oldSections: prevSections, newSections: sections)
|
||||
|
||||
tableView.performBatchUpdates {
|
||||
context.coordinator.sections = appliedDeletes
|
||||
for operation in deleteOperations {
|
||||
applyOperation(operation, tableView: tableView)
|
||||
if let tableView = uiView.viewWithTag(101) as? UITableView {
|
||||
if context.coordinator.sections == sections {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tableView.performBatchUpdates {
|
||||
context.coordinator.sections = appliedDeletesSwapsAndEdits // NOTE: this array already contains necessary edits, but won't be a problem for appplying swaps
|
||||
for operation in swapOperations {
|
||||
applyOperation(operation, tableView: tableView)
|
||||
if context.coordinator.sections == sections {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tableView.performBatchUpdates {
|
||||
context.coordinator.sections = appliedDeletesSwapsAndEdits
|
||||
for operation in editOperations {
|
||||
applyOperation(operation, tableView: tableView)
|
||||
|
||||
let prevSections = context.coordinator.sections
|
||||
let (appliedDeletes, appliedDeletesSwapsAndEdits, deleteOperations, swapOperations, editOperations, insertOperations) = operationsSplit(oldSections: prevSections, newSections: sections)
|
||||
|
||||
tableView.performBatchUpdates {
|
||||
context.coordinator.sections = appliedDeletes
|
||||
for operation in deleteOperations {
|
||||
applyOperation(operation, tableView: tableView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isScrolledToBottom || isScrolledToTop {
|
||||
|
||||
tableView.performBatchUpdates {
|
||||
context.coordinator.sections = appliedDeletesSwapsAndEdits // NOTE: this array already contains necessary edits, but won't be a problem for appplying swaps
|
||||
for operation in swapOperations {
|
||||
applyOperation(operation, tableView: tableView)
|
||||
}
|
||||
}
|
||||
|
||||
tableView.performBatchUpdates {
|
||||
context.coordinator.sections = appliedDeletesSwapsAndEdits
|
||||
for operation in editOperations {
|
||||
applyOperation(operation, tableView: tableView)
|
||||
}
|
||||
}
|
||||
|
||||
context.coordinator.sections = sections
|
||||
|
||||
tableView.beginUpdates()
|
||||
|
|
@ -107,11 +201,11 @@ struct UIList: UIViewRepresentable {
|
|||
applyOperation(operation, tableView: tableView)
|
||||
}
|
||||
tableView.endUpdates()
|
||||
}
|
||||
|
||||
if conversationViewModel.isScrolledToBottom && conversationViewModel.displayedConversationUnreadMessagesCount > 0 {
|
||||
conversationViewModel.markAsRead()
|
||||
conversationsListViewModel.computeChatRoomsList(filter: "")
|
||||
|
||||
if isScrolledToBottom && conversationViewModel.displayedConversationUnreadMessagesCount > 0 {
|
||||
conversationViewModel.markAsRead()
|
||||
conversationsListViewModel.computeChatRoomsList(filter: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -239,12 +333,7 @@ struct UIList: UIViewRepresentable {
|
|||
func makeCoordinator() -> Coordinator {
|
||||
if UIList.sharedCoordinator == nil {
|
||||
UIList.sharedCoordinator = Coordinator(
|
||||
conversationViewModel: conversationViewModel,
|
||||
conversationsListViewModel: conversationsListViewModel,
|
||||
viewModel: viewModel,
|
||||
paginationState: paginationState,
|
||||
isScrolledToTop: $isScrolledToTop,
|
||||
isScrolledToBottom: $isScrolledToBottom,
|
||||
parent: self,
|
||||
geometryProxy: geometryProxy,
|
||||
sections: sections
|
||||
)
|
||||
|
|
@ -254,26 +343,15 @@ struct UIList: UIViewRepresentable {
|
|||
|
||||
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
|
||||
|
||||
var parent: UIList
|
||||
var tableView: UITableView?
|
||||
|
||||
@ObservedObject var viewModel: ChatViewModel
|
||||
@ObservedObject var paginationState: PaginationState
|
||||
@ObservedObject var conversationViewModel: ConversationViewModel
|
||||
@ObservedObject var conversationsListViewModel: ConversationsListViewModel
|
||||
|
||||
@Binding var isScrolledToTop: Bool
|
||||
@Binding var isScrolledToBottom: Bool
|
||||
var floatingButton: FloatingButton?
|
||||
|
||||
var geometryProxy: GeometryProxy
|
||||
var sections: [MessagesSection]
|
||||
|
||||
init(conversationViewModel: ConversationViewModel, conversationsListViewModel: ConversationsListViewModel, viewModel: ChatViewModel, paginationState: PaginationState, isScrolledToTop: Binding<Bool>, isScrolledToBottom: Binding<Bool>, geometryProxy: GeometryProxy, sections: [MessagesSection]) {
|
||||
self.conversationViewModel = conversationViewModel
|
||||
self.conversationsListViewModel = conversationsListViewModel
|
||||
self.viewModel = viewModel
|
||||
self.paginationState = paginationState
|
||||
self._isScrolledToTop = isScrolledToTop
|
||||
self._isScrolledToBottom = isScrolledToBottom
|
||||
init(parent: UIList, geometryProxy: GeometryProxy, sections: [MessagesSection]) {
|
||||
self.parent = parent
|
||||
self.geometryProxy = geometryProxy
|
||||
self.sections = sections
|
||||
|
||||
|
|
@ -283,10 +361,10 @@ struct UIList: UIViewRepresentable {
|
|||
DispatchQueue.main.async {
|
||||
if !self.sections.isEmpty {
|
||||
if self.sections.first != nil
|
||||
&& self.conversationViewModel.conversationMessagesSection.first != nil
|
||||
&& self.conversationViewModel.displayedConversation != nil
|
||||
&& self.sections.first!.chatRoomID == self.conversationViewModel.displayedConversation!.id
|
||||
&& self.sections.first!.rows.count == self.conversationViewModel.conversationMessagesSection.first!.rows.count {
|
||||
&& parent.conversationViewModel.conversationMessagesSection.first != nil
|
||||
&& parent.conversationViewModel.displayedConversation != nil
|
||||
&& self.sections.first!.chatRoomID == parent.conversationViewModel.displayedConversation!.id
|
||||
&& self.sections.first!.rows.count == parent.conversationViewModel.conversationMessagesSection.first!.rows.count {
|
||||
self.tableView!.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
|
||||
}
|
||||
}
|
||||
|
|
@ -297,10 +375,10 @@ struct UIList: UIViewRepresentable {
|
|||
DispatchQueue.main.async {
|
||||
if !self.sections.isEmpty {
|
||||
if self.sections.first != nil
|
||||
&& self.conversationViewModel.conversationMessagesSection.first != nil
|
||||
&& self.conversationViewModel.displayedConversation != nil
|
||||
&& self.sections.first!.chatRoomID == self.conversationViewModel.displayedConversation!.id
|
||||
&& self.sections.first!.rows.count == self.conversationViewModel.conversationMessagesSection.first!.rows.count {
|
||||
&& parent.conversationViewModel.conversationMessagesSection.first != nil
|
||||
&& parent.conversationViewModel.displayedConversation != nil
|
||||
&& self.sections.first!.chatRoomID == parent.conversationViewModel.displayedConversation!.id
|
||||
&& self.sections.first!.rows.count == parent.conversationViewModel.conversationMessagesSection.first!.rows.count {
|
||||
if let dict = notification.userInfo as NSDictionary? {
|
||||
if let index = dict["index"] as? Int {
|
||||
if let animated = dict["animated"] as? Bool {
|
||||
|
|
@ -331,8 +409,8 @@ struct UIList: UIViewRepresentable {
|
|||
}
|
||||
|
||||
func progressView(_ section: Int) -> UIView? {
|
||||
if section > conversationViewModel.conversationMessagesSection.count
|
||||
&& conversationViewModel.conversationMessagesSection[section].rows.count < conversationViewModel.displayedConversationHistorySize {
|
||||
if section > parent.conversationViewModel.conversationMessagesSection.count
|
||||
&& parent.conversationViewModel.conversationMessagesSection[section].rows.count < parent.conversationViewModel.displayedConversationHistorySize {
|
||||
let header = UIHostingController(rootView:
|
||||
ProgressView()
|
||||
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
|
||||
|
|
@ -352,7 +430,7 @@ struct UIList: UIViewRepresentable {
|
|||
let row = sections[indexPath.section].rows[indexPath.row]
|
||||
if #available(iOS 16.0, *) {
|
||||
tableViewCell.contentConfiguration = UIHostingConfiguration {
|
||||
ChatBubbleView(conversationViewModel: conversationViewModel, eventLogMessage: row, geometryProxy: geometryProxy)
|
||||
ChatBubbleView(conversationViewModel: parent.conversationViewModel, eventLogMessage: row, geometryProxy: geometryProxy)
|
||||
.padding(.vertical, 2)
|
||||
.padding(.horizontal, 10)
|
||||
.onTapGesture { }
|
||||
|
|
@ -368,32 +446,39 @@ struct UIList: UIViewRepresentable {
|
|||
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
let row = sections[indexPath.section].rows[indexPath.row]
|
||||
paginationState.handle(row.message)
|
||||
parent.paginationState.handle(row.message)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
self.isScrolledToBottom = scrollView.contentOffset.y <= 10
|
||||
|
||||
if self.isScrolledToBottom != self.conversationViewModel.isScrolledToBottom {
|
||||
self.conversationViewModel.isScrolledToBottom = self.isScrolledToBottom
|
||||
let isScrolledToBottomTmp = scrollView.contentOffset.y <= 10
|
||||
DispatchQueue.main.async {
|
||||
if self.parent.isScrolledToBottom != isScrolledToBottomTmp {
|
||||
self.parent.isScrolledToBottom = isScrolledToBottomTmp
|
||||
|
||||
if self.parent.isScrolledToBottom {
|
||||
self.floatingButton!.isHidden = true
|
||||
} else {
|
||||
self.floatingButton!.isHidden = false
|
||||
}
|
||||
|
||||
if self.parent.isScrolledToBottom && self.parent.conversationViewModel.displayedConversationUnreadMessagesCount > 0 {
|
||||
self.parent.conversationViewModel.markAsRead()
|
||||
self.parent.conversationsListViewModel.computeChatRoomsList(filter: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.conversationViewModel.isScrolledToBottom && self.conversationViewModel.displayedConversationUnreadMessagesCount > 0 {
|
||||
self.conversationViewModel.markAsRead()
|
||||
self.conversationsListViewModel.computeChatRoomsList(filter: "")
|
||||
if !parent.isScrolledToTop && scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.frame.height - 500 {
|
||||
parent.conversationViewModel.getOldMessages()
|
||||
}
|
||||
|
||||
if !self.isScrolledToTop && scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.frame.height - 500 {
|
||||
self.conversationViewModel.getOldMessages()
|
||||
}
|
||||
|
||||
self.isScrolledToTop = scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.frame.height - 500
|
||||
parent.isScrolledToTop = scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.frame.height - 500
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
|
||||
let archiveAction = UIContextualAction(style: .normal, title: "") { action, view, completionHandler in
|
||||
self.conversationViewModel.replyToMessage(index: indexPath.row)
|
||||
self.parent.conversationViewModel.replyToMessage(index: indexPath.row)
|
||||
completionHandler(true)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,8 +48,6 @@ class ConversationViewModel: ObservableObject {
|
|||
@Published var selectedMessage: EventLogMessage?
|
||||
@Published var messageToReply: EventLogMessage?
|
||||
|
||||
@Published var isScrolledToBottom: Bool = true
|
||||
|
||||
init() {}
|
||||
|
||||
func addConversationDelegate() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue