Skip to content
Snippets Groups Projects
Commit 80b177b0 authored by Bruno Muniz's avatar Bruno Muniz :apple:
Browse files

Working on fixing the changes

parent ed0d88ce
No related branches found
No related tags found
2 merge requests!40v1.1.2b166,!38Using new database structure
Showing
with 630 additions and 541 deletions
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1340"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Integration"
BuildableName = "Integration"
BlueprintName = "Integration"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Integration"
BuildableName = "Integration"
BlueprintName = "Integration"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
......@@ -3,34 +3,34 @@
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string></string>
<string>662236151640-herpu89qikpfs9m4kvbi9bs5fpdji5de.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string></string>
<string>com.googleusercontent.apps.662236151640-herpu89qikpfs9m4kvbi9bs5fpdji5de</string>
<key>ANDROID_CLIENT_ID</key>
<string></string>
<string>662236151640-2ughgo2dvc59dm4o39b45lbdungp2mct.apps.googleusercontent.com</string>
<key>API_KEY</key>
<string></string>
<string>AIzaSyCbI2yQ7pbuVSRvraqanjGcS9CDrjD7lNU</string>
<key>GCM_SENDER_ID</key>
<string></string>
<string>662236151640</string>
<key>PLIST_VERSION</key>
<string></string>
<string>1</string>
<key>BUNDLE_ID</key>
<string></string>
<string>io.xxlabs.messenger</string>
<key>PROJECT_ID</key>
<string></string>
<string>xx-messenger-6e03e</string>
<key>STORAGE_BUCKET</key>
<string></string>
<string>xx-messenger-6e03e.appspot.com</string>
<key>IS_ADS_ENABLED</key>
<false/>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false/>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<false/>
<true></true>
<key>IS_GCM_ENABLED</key>
<false/>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<false/>
<true></true>
<key>GOOGLE_APP_ID</key>
<string></string>
<string>1:662236151640:ios:24badb58ab07515d8cef2d</string>
</dict>
</plist>
import UIKit
import BackgroundTasks
import XXModels
import Theme
import XXLogger
import Defaults
......@@ -91,7 +92,11 @@ public class AppDelegate: UIResponder, UIApplicationDelegate {
guard UIApplication.shared.backgroundTimeRemaining > 9 else {
if !self.forceFailedPendingMessages {
self.forceFailedPendingMessages = true
session.forceFailMessages()
// TODO: We need a Message.Assignment for status
// let query = Message.Query(status: [.sending])
// let assignment = Message.Assignments(status: .sendingFailed)
// _ = try? session.dbManager.bulkUpdateMessages(query, assignment)
}
return
......
......@@ -259,7 +259,7 @@ extension PushRouter {
}
case .contactChat(id: let id):
if let session = try? DependencyInjection.Container.shared.resolve() as SessionType,
let contact = session.getContactWith(userId: id) {
let contact = try? session.dbManager.fetchContacts(.init(id: [id])).first {
navigationController.setViewControllers([
ChatListController(),
SingleChatController(contact)
......@@ -267,7 +267,7 @@ extension PushRouter {
}
case .groupChat(id: let id):
if let session = try? DependencyInjection.Container.shared.resolve() as SessionType,
let info = session.getGroupChatInfoWith(groupId: id) {
let info = try? session.dbManager.fetchGroupInfos(.init(groupId: id)).first {
navigationController.setViewControllers([
ChatListController(),
GroupChatController(info)
......
......@@ -33,16 +33,16 @@ public final class GroupChatController: UIViewController {
private let layoutDelegate = LayoutDelegate()
private var cancellables = Set<AnyCancellable>()
private var drawerCancellables = Set<AnyCancellable>()
private var sections = [ArraySection<ChatSection, ChatItem>]()
private var sections = [ArraySection<ChatSection, Message>]()
private var currentInterfaceActions = SetActor<Set<InterfaceActions>, ReactionTypes>()
public override var canBecomeFirstResponder: Bool { true }
public override var inputAccessoryView: UIView? { inputComponent }
public init(_ info: GroupChatInfo) {
public init(_ info: GroupInfo) {
let viewModel = GroupChatViewModel(info)
self.viewModel = viewModel
self.members = .init(with: [])
self.members = .init(with: info.members)
self.inputComponent = ChatInputView(store: .init(
initialState: .init(canAddAttachments: false),
......@@ -60,7 +60,14 @@ public final class GroupChatController: UIViewController {
super.init(nibName: nil, bundle: nil)
// header.setup(title: info.group.name, memberList: info.members.map { ($0.username, $0.photo) })
let memberList = info.members.map {
Member(
title: ($0.nickname ?? $0.username) ?? "Fetching username...",
photo: $0.photo
)
}
header.setup(title: info.group.name, memberList: memberList)
}
public required init?(coder: NSCoder) { nil }
......@@ -155,7 +162,9 @@ public final class GroupChatController: UIViewController {
viewModel.replyPublisher
.receive(on: DispatchQueue.main)
.sink { [unowned self] in inputComponent.setupReply(message: $0.text, sender: $0.sender) }
.sink { [unowned self] senderTitle, messageText in
inputComponent.setupReply(message: messageText, sender: senderTitle)
}
.store(in: &cancellables)
}
......@@ -328,7 +337,7 @@ extension GroupChatController: UICollectionViewDataSource {
let showRound: (String?) -> Void = viewModel.showRoundFrom(_:)
if item.status == .received {
if item.payload.reply != nil {
if item.replyMessageId != nil {
let cell: IncomingGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
// Bubbler.buildReplyGroup(
......@@ -356,7 +365,7 @@ extension GroupChatController: UICollectionViewDataSource {
return cell
}
} else if item.status == .sendingFailed {
if item.payload.reply != nil {
if item.replyMessageId != nil {
let cell: OutgoingFailedGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
// Bubbler.buildReplyGroup(
......@@ -383,7 +392,7 @@ extension GroupChatController: UICollectionViewDataSource {
return cell
}
} else {
if item.payload.reply != nil {
if item.replyMessageId != nil {
let cell: OutgoingGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
// Bubbler.buildReplyGroup(
......@@ -524,7 +533,7 @@ extension GroupChatController: UICollectionViewDelegate {
let item = self.sections[indexPath.section].elements[indexPath.item]
let copy = UIAction(title: Localized.Chat.BubbleMenu.copy, state: .off) { _ in
UIPasteboard.general.string = item.payload.text
UIPasteboard.general.string = item.text
}
let reply = UIAction(title: Localized.Chat.BubbleMenu.reply, state: .off) { [weak self] _ in
......
......@@ -6,9 +6,9 @@ import XXModels
final class MembersController: UIViewController {
lazy private var stackView = UIStackView()
private let members: [GroupMember]
private let members: [Contact]
init(with members: [GroupMember]) {
init(with members: [Contact]) {
self.members = members
super.init(nibName: nil, bundle: nil)
}
......@@ -26,16 +26,17 @@ final class MembersController: UIViewController {
stackView.distribution = .fillEqually
view.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(10)
make.left.right.equalToSuperview()
make.bottom.equalTo(view.safeAreaLayoutGuide)
stackView.snp.makeConstraints {
$0.top.equalToSuperview().offset(10)
$0.left.right.equalToSuperview()
$0.bottom.equalTo(view.safeAreaLayoutGuide)
}
for member in members {
members.forEach {
let memberView = MemberView()
// memberView.titleLabel.text = member.username
// memberView.avatarView.setupProfile(title: member.username, image: member.photo, size: .small)
let assignedTitle = ($0.nickname ?? $0.username) ?? "Fetching username..."
memberView.titleLabel.text = assignedTitle
memberView.avatarView.setupProfile(title: assignedTitle, image: $0.photo, size: .small)
stackView.addArrangedSubview(memberView)
}
}
......@@ -57,24 +58,24 @@ private final class MemberView: UIView {
addSubview(avatarView)
addSubview(separatorView)
avatarView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(10)
make.width.height.equalTo(30)
make.left.equalToSuperview().offset(25)
make.centerY.equalToSuperview()
avatarView.snp.makeConstraints {
$0.top.equalToSuperview().offset(10)
$0.width.height.equalTo(30)
$0.left.equalToSuperview().offset(25)
$0.centerY.equalToSuperview()
}
titleLabel.snp.makeConstraints { make in
make.centerY.equalTo(avatarView)
make.left.equalTo(avatarView.snp.right).offset(14)
make.right.lessThanOrEqualToSuperview().offset(-10)
titleLabel.snp.makeConstraints {
$0.centerY.equalTo(avatarView)
$0.left.equalTo(avatarView.snp.right).offset(14)
$0.right.lessThanOrEqualToSuperview().offset(-10)
}
separatorView.snp.makeConstraints { make in
make.height.equalTo(1)
make.left.equalToSuperview().offset(25)
make.right.equalToSuperview()
make.bottom.equalToSuperview()
separatorView.snp.makeConstraints {
$0.height.equalTo(1)
$0.left.equalToSuperview().offset(25)
$0.right.equalToSuperview()
$0.bottom.equalToSuperview()
}
}
......
......@@ -19,6 +19,10 @@ extension FlexibleSpace: CollectionCellContent {
func prepareForReuse() {}
}
extension Message: Differentiable {
public var differenceIdentifier: Int64 { id! }
}
public final class SingleChatController: UIViewController {
@Dependency private var hud: HUDType
@Dependency private var logger: XXLogger
......@@ -44,7 +48,7 @@ public final class SingleChatController: UIViewController {
private let layoutDelegate = LayoutDelegate()
private var cancellables = Set<AnyCancellable>()
private var drawerCancellables = Set<AnyCancellable>()
private var sections = [ArraySection<ChatSection, ChatItem>]()
private var sections = [ArraySection<ChatSection, Message>]()
private var currentInterfaceActions: SetActor<Set<InterfaceActions>, ReactionTypes> = SetActor()
var fileURL: URL?
......@@ -204,7 +208,9 @@ public final class SingleChatController: UIViewController {
viewModel.replyPublisher
.receive(on: DispatchQueue.main)
.sink { [unowned self] in inputComponent.setupReply(message: $0.text, sender: $0.sender) }
.sink { [unowned self] senderTitle, messageText in
inputComponent.setupReply(message: messageText, sender: senderTitle)
}
.store(in: &cancellables)
viewModel.navigation
......
import Models
import XXModels
import Foundation
import DifferenceKit
struct ChatItem: Equatable, Differentiable {
let date: Date
var uniqueId: Data?
let identity: Int64
var roundURL: String?
let payload: Payload
var status: Message.Status
var differenceIdentifier: Int64 { identity }
init(_ message: Message) {
self.identity = message.id!
self.status = message.status
self.payload = Payload(text: message.text, reply: nil)
self.roundURL = message.roundURL
self.uniqueId = message.networkId
self.date = message.date
}
}
......@@ -2,11 +2,6 @@ import Foundation
import DifferenceKit
struct ChatSection: Equatable, Differentiable {
// MARK: Properties
var date: Date
// MARK: DifferenceKit
var differenceIdentifier: Date { date }
}
......@@ -15,7 +15,7 @@ enum GroupChatNavigationRoutes: Equatable {
final class GroupChatViewModel {
@Dependency private var session: SessionType
var replyPublisher: AnyPublisher<ReplyModel, Never> {
var replyPublisher: AnyPublisher<(String, String), Never> {
replySubject.eraseToAnyPublisher()
}
......@@ -23,18 +23,17 @@ final class GroupChatViewModel {
routesSubject.eraseToAnyPublisher()
}
let info: GroupChatInfo
let info: GroupInfo
private var stagedReply: Reply?
private var cancellables = Set<AnyCancellable>()
private let replySubject = PassthroughSubject<ReplyModel, Never>()
private let replySubject = PassthroughSubject<(String, String), Never>()
private let routesSubject = PassthroughSubject<GroupChatNavigationRoutes, Never>()
var messages: AnyPublisher<[ArraySection<ChatSection, ChatItem>], Never> {
var messages: AnyPublisher<[ArraySection<ChatSection, Message>], Never> {
session.dbManager.fetchMessagesPublisher(.init(chat: .group(info.group.id)))
.assertNoFailure()
.map { messages -> [ArraySection<ChatSection, ChatItem>] in
let domainModels = messages.map { ChatItem($0) }
let groupedByDate = Dictionary(grouping: domainModels) { domainModel -> Date in
.map { messages -> [ArraySection<ChatSection, Message>] in
let groupedByDate = Dictionary(grouping: messages) { domainModel -> Date in
let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date)
return Calendar.current.date(from: components)!
}
......@@ -43,14 +42,14 @@ final class GroupChatViewModel {
.map { .init(model: ChatSection(date: $0.key), elements: $0.value) }
.sorted(by: { $0.model.date < $1.model.date })
}
.map { sections -> [ArraySection<ChatSection, ChatItem>] in
var snapshot = [ArraySection<ChatSection, ChatItem>]()
.map { sections -> [ArraySection<ChatSection, Message>] in
var snapshot = [ArraySection<ChatSection, Message>]()
sections.forEach { snapshot.append(.init(model: $0.model, elements: $0.elements)) }
return snapshot
}.eraseToAnyPublisher()
}
init(_ info: GroupChatInfo) {
init(_ info: GroupInfo) {
self.info = info
}
......@@ -60,8 +59,8 @@ final class GroupChatViewModel {
_ = try? session.dbManager.bulkUpdateMessages(query, assignment)
}
func didRequestDelete(_ items: [ChatItem]) {
// try? session.dbManager.deleteMessages(.init(id: items.map(\.identity)))
func didRequestDelete(_ messages: [Message]) {
_ = try? session.dbManager.deleteMessages(.init(id: Set(messages.map(\.id))))
}
func send(_ text: String) {
......@@ -72,8 +71,9 @@ final class GroupChatViewModel {
stagedReply = nil
}
func retry(_ model: ChatItem) {
// session.retryGroupMessage(model.identity)
func retry(_ message: Message) {
guard let id = message.id else { return }
session.retryMessage(id)
}
func showRoundFrom(_ roundURL: String?) {
......@@ -89,20 +89,24 @@ final class GroupChatViewModel {
}
func getName(from senderId: Data) -> String {
fatalError()
// guard let member = info.members.first(where: { $0.userId == senderId }) else { return "You" }
// return member.username
guard let contact = try? session.dbManager.fetchContacts(.init(id: [senderId])).first else {
return "You"
}
return (contact.nickname ?? contact.username) ?? "Fetching username..."
}
func getText(from messageId: Data) -> String {
fatalError()
// session.getTextFromGroupMessage(messageId: messageId) ?? "[DELETED]"
guard let message = try? session.dbManager.fetchMessages(.init(networkId: messageId)).first else {
return "[DELETED]"
}
// func didRequestReply(_ model: GroupChatItem) {
// guard let messageId = model.uniqueId else { return }
//
// stagedReply = Reply(messageId: messageId, senderId: model.sender)
// replySubject.send(.init(text: model.payload.text, sender: getName(from: model.sender)))
// }
return message.text
}
func didRequestReply(_ message: Message) {
guard let networkId = message.networkId else { return }
stagedReply = Reply(messageId: networkId, senderId: message.senderId)
replySubject.send((getName(from: message.senderId), message.text))
}
}
......@@ -11,11 +11,6 @@ import Permissions
import DifferenceKit
import DependencyInjection
struct ReplyModel {
var text: String
var sender: String
}
enum SingleChatNavigationRoutes: Equatable {
case none
case camera
......@@ -36,22 +31,22 @@ final class SingleChatViewModel {
private var stagedReply: Reply?
private var cancellables = Set<AnyCancellable>()
private let contactSubject: CurrentValueSubject<Contact, Never>
private let replySubject = PassthroughSubject<ReplyModel, Never>()
private let replySubject = PassthroughSubject<(String, String), Never>()
private let navigationRoutes = PassthroughSubject<SingleChatNavigationRoutes, Never>()
private let sectionsRelay = CurrentValueSubject<[ArraySection<ChatSection, ChatItem>], Never>([])
private let sectionsRelay = CurrentValueSubject<[ArraySection<ChatSection, Message>], Never>([])
var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() }
private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none)
var isOnline: AnyPublisher<Bool, Never> { session.isOnline }
var contactPublisher: AnyPublisher<Contact, Never> { contactSubject.eraseToAnyPublisher() }
var replyPublisher: AnyPublisher<ReplyModel, Never> { replySubject.eraseToAnyPublisher() }
var replyPublisher: AnyPublisher<(String, String), Never> { replySubject.eraseToAnyPublisher() }
var navigation: AnyPublisher<SingleChatNavigationRoutes, Never> { navigationRoutes.eraseToAnyPublisher() }
var shouldDisplayEmptyView: AnyPublisher<Bool, Never> { sectionsRelay.map { $0.isEmpty }.eraseToAnyPublisher() }
var messages: AnyPublisher<[ArraySection<ChatSection, ChatItem>], Never> {
sectionsRelay.map { sections -> [ArraySection<ChatSection, ChatItem>] in
var snapshot = [ArraySection<ChatSection, ChatItem>]()
var messages: AnyPublisher<[ArraySection<ChatSection, Message>], Never> {
sectionsRelay.map { sections -> [ArraySection<ChatSection, Message>] in
var snapshot = [ArraySection<ChatSection, Message>]()
sections.forEach { snapshot.append(.init(model: $0.model, elements: $0.elements)) }
return snapshot
}.eraseToAnyPublisher()
......@@ -82,10 +77,8 @@ final class SingleChatViewModel {
session.dbManager.fetchMessagesPublisher(.init(chat: .direct(session.myId, contact.id)))
.assertNoFailure()
.map { messages in
let domainModels = messages.map { ChatItem($0) }
let groupedByDate = Dictionary(grouping: domainModels) { domainModel -> Date in
.map {
let groupedByDate = Dictionary(grouping: $0) { domainModel -> Date in
let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date)
return Calendar.current.date(from: components)!
}
......@@ -133,8 +126,9 @@ final class SingleChatViewModel {
_ = try? session.dbManager.deleteMessages(.init(chat: .direct(session.myId, contact.id)))
}
func didRequestRetry(_ model: ChatItem) {
session.retryMessage(model.identity)
func didRequestRetry(_ message: Message) {
guard let id = message.id else { return }
session.retryMessage(id)
}
func didNavigateSomewhere() {
......@@ -167,11 +161,11 @@ final class SingleChatViewModel {
return false
}
func didRequestCopy(_ model: ChatItem) {
UIPasteboard.general.string = model.payload.text
func didRequestCopy(_ model: Message) {
UIPasteboard.general.string = model.text
}
func didRequestDeleteSingle(_ model: ChatItem) {
func didRequestDeleteSingle(_ model: Message) {
didRequestDelete([model])
}
......@@ -186,17 +180,25 @@ final class SingleChatViewModel {
stagedReply = nil
}
func didRequestReply(_ model: ChatItem) {
// guard let messageId = model.uniqueId else { return }
//
// let isIncoming = model.status == .received
// stagedReply = Reply(messageId: messageId, senderId: isIncoming ? contact.userId : session.myId)
// replySubject.send(.init(text: model.payload.text, sender: isIncoming ? contact.nickname ?? contact.username : "You"))
func didRequestReply(_ message: Message) {
let senderTitle: String = {
if message.senderId == session.myId {
return "You"
} else {
return (contact.nickname ?? contact.username) ?? "Fetching username..."
}
}()
replySubject.send((senderTitle, message.text))
stagedReply = Reply(messageId: message.networkId!, senderId: message.senderId)
}
func getText(from messageId: Data) -> String {
fatalError()
// session.getTextFromMessage(messageId: messageId) ?? "[DELETED]"
guard let message = try? session.dbManager.fetchMessages(.init(networkId: messageId)).first else {
return "[DELETED]"
}
return message.text
}
func showRoundFrom(_ roundURL: String?) {
......@@ -207,19 +209,19 @@ final class SingleChatViewModel {
}
}
func didRequestDelete(_ items: [ChatItem]) {
///session.delete(messages: items.map { $0.identity })
func didRequestDelete(_ items: [Message]) {
_ = try? session.dbManager.deleteMessages(.init(id: Set(items.compactMap(\.id))))
}
func itemWith(id: Int64) -> ChatItem? {
sectionsRelay.value.flatMap(\.elements).first(where: { $0.identity == id })
func itemWith(id: Int64) -> Message? {
sectionsRelay.value.flatMap(\.elements).first(where: { $0.id == id })
}
func getName(from senderId: Data) -> String {
senderId == session.myId ? "You" : contact.nickname ?? contact.username!
}
func itemAt(indexPath: IndexPath) -> ChatItem? {
func itemAt(indexPath: IndexPath) -> Message? {
guard sectionsRelay.value.count > indexPath.section else { return nil }
let items = sectionsRelay.value[indexPath.section].elements
......
import UIKit
import Shared
struct Member {
let title: String
let photo: Data?
}
final class GroupHeaderView: UIView {
let titleLabel = UILabel()
let containerView = UIView()
......@@ -39,14 +44,14 @@ final class GroupHeaderView: UIView {
required init?(coder: NSCoder) { nil }
func setup(title: String, memberList: [(String, Data?)]) {
func setup(title: String, memberList: [Member]) {
titleLabel.text = title
for member in memberList {
memberList.forEach {
let avatarView = AvatarView()
avatarView.layer.borderWidth = 3
avatarView.layer.borderColor = UIColor.white.cgColor
avatarView.setupProfile(title: member.0, image: member.1, size: .small)
avatarView.setupProfile(title: $0.title, image: $0.photo, size: .small)
avatarView.snp.makeConstraints { $0.width.height.equalTo(25.0) }
stackView.addArrangedSubview(avatarView)
}
......
......@@ -3,9 +3,16 @@ import Theme
import Models
import Shared
import Combine
import XXModels
import MenuFeature
import DependencyInjection
extension Contact: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
public final class ChatListController: UIViewController {
@Dependency private var coordinator: ChatListCoordinating
@Dependency private var statusBarController: StatusBarStyleControlling
......@@ -69,10 +76,10 @@ public final class ChatListController: UIViewController {
}
}.store(in: &cancellables)
viewModel.badgeCountPublisher
.receive(on: DispatchQueue.main)
.sink { [unowned self] in topLeftView.updateBadge($0) }
.store(in: &cancellables)
// viewModel.badgeCountPublisher
// .receive(on: DispatchQueue.main)
// .sink { [unowned self] in topLeftView.updateBadge($0) }
// .store(in: &cancellables)
topLeftView.actionPublisher
.receive(on: DispatchQueue.main)
......@@ -115,19 +122,19 @@ public final class ChatListController: UIViewController {
collectionView: screenView.listContainerView.collectionView
) { collectionView, indexPath, contact in
let cell: ChatListRecentContactCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
cell.setup(title: contact.nickname ?? contact.username, image: contact.photo)
cell.setup(title: contact.nickname ?? contact.username!, image: contact.photo)
return cell
}
screenView.listContainerView.collectionView.delegate = self
screenView.listContainerView.collectionView.dataSource = collectionDataSource
viewModel.recentsPublisher
.receive(on: DispatchQueue.main)
.sink { [unowned self] in
collectionDataSource.apply($0)
shouldBeShowingRecents = $0.numberOfItems > 0
}.store(in: &cancellables)
// viewModel.recentsPublisher
// .receive(on: DispatchQueue.main)
// .sink { [unowned self] in
// collectionDataSource.apply($0)
// shouldBeShowingRecents = $0.numberOfItems > 0
// }.store(in: &cancellables)
}
private func setupBindings() {
......@@ -146,33 +153,33 @@ public final class ChatListController: UIViewController {
screenView.searchListContainerView.emptyView.updateSearched(content: query)
}.store(in: &cancellables)
Publishers.CombineLatest(
viewModel.searchPublisher,
screenView.searchView.textPublisher.removeDuplicates()
)
.receive(on: DispatchQueue.main)
.sink { [unowned self] items, query in
guard query.isEmpty == false else {
screenView.searchListContainerView.isHidden = true
screenView.listContainerView.isHidden = false
screenView.bringSubviewToFront(screenView.listContainerView)
return
}
screenView.listContainerView.isHidden = true
screenView.searchListContainerView.isHidden = false
guard items.numberOfItems > 0 else {
screenView.searchListContainerView.emptyView.isHidden = false
screenView.bringSubviewToFront(screenView.searchListContainerView)
screenView.searchListContainerView.bringSubviewToFront(screenView.searchListContainerView.emptyView)
return
}
screenView.searchListContainerView.bringSubviewToFront(searchTableController.view)
screenView.searchListContainerView.emptyView.isHidden = true
}
.store(in: &cancellables)
// Publishers.CombineLatest(
// viewModel.searchPublisher,
// screenView.searchView.textPublisher.removeDuplicates()
// )
// .receive(on: DispatchQueue.main)
// .sink { [unowned self] items, query in
// guard query.isEmpty == false else {
// screenView.searchListContainerView.isHidden = true
// screenView.listContainerView.isHidden = false
// screenView.bringSubviewToFront(screenView.listContainerView)
// return
// }
//
// screenView.listContainerView.isHidden = true
// screenView.searchListContainerView.isHidden = false
//
// guard items.numberOfItems > 0 else {
// screenView.searchListContainerView.emptyView.isHidden = false
// screenView.bringSubviewToFront(screenView.searchListContainerView)
// screenView.searchListContainerView.bringSubviewToFront(screenView.searchListContainerView.emptyView)
// return
// }
//
// screenView.searchListContainerView.bringSubviewToFront(searchTableController.view)
// screenView.searchListContainerView.emptyView.isHidden = true
// }
// .store(in: &cancellables)
screenView.searchView
.isEditingPublisher
......@@ -181,19 +188,19 @@ public final class ChatListController: UIViewController {
.sink { [unowned self] in isEditingSearch = $0 }
.store(in: &cancellables)
viewModel.chatsPublisher
.receive(on: DispatchQueue.main)
.sink { [unowned self] in
guard $0.isEmpty == false else {
screenView.listContainerView.bringSubviewToFront(screenView.listContainerView.emptyView)
screenView.listContainerView.emptyView.isHidden = false
return
}
screenView.listContainerView.bringSubviewToFront(tableController.view)
screenView.listContainerView.emptyView.isHidden = true
}
.store(in: &cancellables)
// viewModel.chatsPublisher
// .receive(on: DispatchQueue.main)
// .sink { [unowned self] in
// guard $0.isEmpty == false else {
// screenView.listContainerView.bringSubviewToFront(screenView.listContainerView.emptyView)
// screenView.listContainerView.emptyView.isHidden = false
// return
// }
//
// screenView.listContainerView.bringSubviewToFront(tableController.view)
// screenView.listContainerView.emptyView.isHidden = true
// }
// .store(in: &cancellables)
screenView.searchListContainerView
.emptyView.searchButton
......
......@@ -4,16 +4,16 @@ import Models
import Combine
import DependencyInjection
class ChatSearchListTableViewDiffableDataSource: UITableViewDiffableDataSource<SearchSection, SearchItem> {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch snapshot().sectionIdentifiers[section] {
case .chats:
return "CHATS"
case .connections:
return "CONNECTIONS"
}
}
}
//class ChatSearchListTableViewDiffableDataSource: UITableViewDiffableDataSource<SearchSection, SearchItem> {
// override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
// switch snapshot().sectionIdentifiers[section] {
// case .chats:
// return "CHATS"
// case .connections:
// return "CONNECTIONS"
// }
// }
//}
final class ChatSearchTableController: UITableViewController {
@Dependency private var coordinator: ChatListCoordinating
......@@ -21,65 +21,65 @@ final class ChatSearchTableController: UITableViewController {
private let viewModel: ChatListViewModel
private let cellHeight: CGFloat = 83.0
private var cancellables = Set<AnyCancellable>()
private var tableDataSource: ChatSearchListTableViewDiffableDataSource?
// private var tableDataSource: ChatSearchListTableViewDiffableDataSource?
init(_ viewModel: ChatListViewModel) {
self.viewModel = viewModel
super.init(style: .grouped)
tableDataSource = ChatSearchListTableViewDiffableDataSource(
tableView: tableView
) { table, indexPath, item in
let cell = table.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self)
switch item {
case .chat(let subitem):
if case .contact(let info) = subitem {
cell.setupContact(
name: info.contact.nickname ?? info.contact.username,
image: info.contact.photo,
date: Date.fromTimestamp(info.lastMessage!.timestamp),
hasUnread: info.lastMessage!.unread,
preview: info.lastMessage!.payload.text
)
}
if case .group(let info) = subitem {
let date: Date = {
guard let lastMessage = info.lastMessage else {
return info.group.createdAt
}
return Date.fromTimestamp(lastMessage.timestamp)
}()
let hasUnread: Bool = {
guard let lastMessage = info.lastMessage else {
return false
}
return lastMessage.unread
}()
cell.setupGroup(
name: info.group.name,
date: date,
preview: info.lastMessage?.payload.text,
hasUnread: hasUnread
)
}
case .connection(let contact):
cell.setupContact(
name: contact.nickname ?? contact.username,
image: contact.photo,
date: nil,
hasUnread: false,
preview: contact.username
)
}
return cell
}
// tableDataSource = ChatSearchListTableViewDiffableDataSource(
// tableView: tableView
// ) { table, indexPath, item in
// let cell = table.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self)
// switch item {
// case .chat(let subitem):
// if case .contact(let info) = subitem {
// cell.setupContact(
// name: info.contact.nickname ?? info.contact.username,
// image: info.contact.photo,
// date: Date.fromTimestamp(info.lastMessage!.timestamp),
// hasUnread: info.lastMessage!.unread,
// preview: info.lastMessage!.payload.text
// )
// }
//
// if case .group(let info) = subitem {
// let date: Date = {
// guard let lastMessage = info.lastMessage else {
// return info.group.createdAt
// }
//
// return Date.fromTimestamp(lastMessage.timestamp)
// }()
//
// let hasUnread: Bool = {
// guard let lastMessage = info.lastMessage else {
// return false
// }
//
// return lastMessage.unread
// }()
//
// cell.setupGroup(
// name: info.group.name,
// date: date,
// preview: info.lastMessage?.payload.text,
// hasUnread: hasUnread
// )
// }
//
// case .connection(let contact):
// cell.setupContact(
// name: contact.nickname ?? contact.username!,
// image: contact.photo,
// date: nil,
// hasUnread: false,
// preview: contact.username!
// )
// }
//
// return cell
// }
}
required init?(coder: NSCoder) { nil }
......@@ -91,13 +91,13 @@ final class ChatSearchTableController: UITableViewController {
tableView.tableFooterView = UIView()
tableView.sectionIndexColor = .blue
tableView.register(ChatListCell.self)
tableView.dataSource = tableDataSource
// tableView.dataSource = tableDataSource
view.backgroundColor = Asset.neutralWhite.color
viewModel.searchPublisher
.receive(on: DispatchQueue.main)
.sink { [unowned self] in tableDataSource?.apply($0, animatingDifferences: false) }
.store(in: &cancellables)
// viewModel.searchPublisher
// .receive(on: DispatchQueue.main)
// .sink { [unowned self] in tableDataSource?.apply($0, animatingDifferences: false) }
// .store(in: &cancellables)
}
}
......@@ -110,19 +110,19 @@ extension ChatSearchTableController {
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let item = tableDataSource?.itemIdentifier(for: indexPath) {
switch item {
case .chat(let chat):
switch chat {
case .contact(let info):
guard info.contact.status == .friend else { return }
coordinator.toSingleChat(with: info.contact, from: self)
case .group(let info):
coordinator.toGroupChat(with: info, from: self)
}
case .connection(let contact):
coordinator.toContact(contact, from: self)
}
}
// if let item = tableDataSource?.itemIdentifier(for: indexPath) {
// switch item {
// case .chat(let chat):
// switch chat {
// case .contact(let info):
// guard info.contact.status == .friend else { return }
// coordinator.toSingleChat(with: info.contact, from: self)
// case .group(let info):
// coordinator.toGroupChat(with: info, from: self)
// }
// case .connection(let contact):
// coordinator.toContact(contact, from: self)
// }
// }
}
}
......@@ -9,7 +9,7 @@ import DependencyInjection
final class ChatListTableController: UITableViewController {
@Dependency private var coordinator: ChatListCoordinating
private var rows = [Chat]()
private var rows = [Any]()
private let viewModel: ChatListViewModel
private let cellHeight: CGFloat = 83.0
private var cancellables = Set<AnyCancellable>()
......@@ -31,28 +31,28 @@ final class ChatListTableController: UITableViewController {
tableView.register(ChatListCell.self)
tableView.tableFooterView = UIView()
viewModel
.chatsPublisher
.receive(on: DispatchQueue.main)
.sink { [unowned self] in
guard !self.rows.isEmpty else {
self.rows = $0
tableView.reloadData()
return
}
self.tableView.reload(
using: StagedChangeset(source: self.rows, target: $0),
deleteSectionsAnimation: .automatic,
insertSectionsAnimation: .automatic,
reloadSectionsAnimation: .none,
deleteRowsAnimation: .automatic,
insertRowsAnimation: .automatic,
reloadRowsAnimation: .none
) { [unowned self] in
self.rows = $0
}
}.store(in: &cancellables)
// viewModel
// .chatsPublisher
// .receive(on: DispatchQueue.main)
// .sink { [unowned self] in
// guard !self.rows.isEmpty else {
// self.rows = $0
// tableView.reloadData()
// return
// }
//
// self.tableView.reload(
// using: StagedChangeset(source: self.rows, target: $0),
// deleteSectionsAnimation: .automatic,
// insertSectionsAnimation: .automatic,
// reloadSectionsAnimation: .none,
// deleteRowsAnimation: .automatic,
// insertRowsAnimation: .automatic,
// reloadRowsAnimation: .none
// ) { [unowned self] in
// self.rows = $0
// }
// }.store(in: &cancellables)
}
}
......@@ -78,7 +78,7 @@ extension ChatListTableController {
let delete = UIContextualAction(style: .normal, title: nil) { [weak self] _, _, complete in
guard let self = self else { return }
self.didRequestDeletionOf(self.rows[indexPath.row])
// self.didRequestDeletionOf(self.rows[indexPath.row])
complete(true)
}
......@@ -88,13 +88,13 @@ extension ChatListTableController {
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch rows[indexPath.row] {
case .contact(let info):
guard info.contact.status == .friend else { return }
coordinator.toSingleChat(with: info.contact, from: self)
case .group(let info):
coordinator.toGroupChat(with: info, from: self)
}
// switch rows[indexPath.row] {
// case .contact(let info):
// guard info.contact.status == .friend else { return }
// coordinator.toSingleChat(with: info.contact, from: self)
// case .group(let info):
// coordinator.toGroupChat(with: info, from: self)
// }
}
override func tableView(
......@@ -102,96 +102,97 @@ extension ChatListTableController {
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self)
if case .contact(let info) = rows[indexPath.row] {
cell.setupContact(
name: info.contact.nickname ?? info.contact.username,
image: info.contact.photo,
date: Date.fromTimestamp(info.lastMessage!.timestamp),
hasUnread: info.lastMessage!.unread,
preview: info.lastMessage!.payload.text
)
}
if case .group(let info) = rows[indexPath.row] {
let date: Date = {
guard let lastMessage = info.lastMessage else {
return info.group.createdAt
}
return Date.fromTimestamp(lastMessage.timestamp)
}()
let hasUnread: Bool = {
guard let lastMessage = info.lastMessage else {
return false
}
return lastMessage.unread
}()
cell.setupGroup(
name: info.group.name,
date: date,
preview: info.lastMessage?.payload.text,
hasUnread: hasUnread
)
}
return cell
}
private func didRequestDeletionOf(_ item: Chat) {
let title: String
let subtitle: String
let actionTitle: String
let actionClosure: () -> Void
switch item {
case .group(let info):
title = Localized.ChatList.DeleteGroup.title
subtitle = Localized.ChatList.DeleteGroup.subtitle
actionTitle = Localized.ChatList.DeleteGroup.action
actionClosure = { [weak viewModel] in viewModel?.leave(info.group) }
case .contact(let info):
title = Localized.ChatList.Delete.title
subtitle = Localized.ChatList.Delete.subtitle
actionTitle = Localized.ChatList.Delete.delete
actionClosure = { [weak viewModel] in viewModel?.clear(info.contact) }
}
let actionButton = DrawerCapsuleButton(model: .init(title: actionTitle, style: .red))
let drawer = DrawerController(with: [
DrawerText(
font: Fonts.Mulish.bold.font(size: 26.0),
text: title,
color: Asset.neutralActive.color,
alignment: .left,
spacingAfter: 19
),
DrawerText(
font: Fonts.Mulish.regular.font(size: 16.0),
text: subtitle,
color: Asset.neutralBody.color,
alignment: .left,
lineHeightMultiple: 1.1,
spacingAfter: 39
),
actionButton
])
actionButton.action.receive(on: DispatchQueue.main)
.sink {
drawer.dismiss(animated: true) { [weak self] in
guard let self = self else { return }
self.drawerCancellables.removeAll()
actionClosure()
}
}.store(in: &drawerCancellables)
coordinator.toDrawer(drawer, from: self)
}
// let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self)
//
// if case .contact(let info) = rows[indexPath.row] {
// cell.setupContact(
// name: info.contact.nickname ?? info.contact.username,
// image: info.contact.photo,
// date: Date.fromTimestamp(info.lastMessage!.timestamp),
// hasUnread: info.lastMessage!.unread,
// preview: info.lastMessage!.payload.text
// )
// }
//
// if case .group(let info) = rows[indexPath.row] {
// let date: Date = {
// guard let lastMessage = info.lastMessage else {
// return info.group.createdAt
// }
//
// return Date.fromTimestamp(lastMessage.timestamp)
// }()
//
// let hasUnread: Bool = {
// guard let lastMessage = info.lastMessage else {
// return false
// }
//
// return lastMessage.unread
// }()
//
// cell.setupGroup(
// name: info.group.name,
// date: date,
// preview: info.lastMessage?.payload.text,
// hasUnread: hasUnread
// )
// }
//
// return cell
fatalError()
}
// private func didRequestDeletionOf(_ item: Chat) {
// let title: String
// let subtitle: String
// let actionTitle: String
// let actionClosure: () -> Void
//
// switch item {
// case .group(let info):
// title = Localized.ChatList.DeleteGroup.title
// subtitle = Localized.ChatList.DeleteGroup.subtitle
// actionTitle = Localized.ChatList.DeleteGroup.action
// actionClosure = { [weak viewModel] in viewModel?.leave(info.group) }
//
// case .contact(let info):
// title = Localized.ChatList.Delete.title
// subtitle = Localized.ChatList.Delete.subtitle
// actionTitle = Localized.ChatList.Delete.delete
// actionClosure = { [weak viewModel] in viewModel?.clear(info.contact) }
// }
//
// let actionButton = DrawerCapsuleButton(model: .init(title: actionTitle, style: .red))
//
// let drawer = DrawerController(with: [
// DrawerText(
// font: Fonts.Mulish.bold.font(size: 26.0),
// text: title,
// color: Asset.neutralActive.color,
// alignment: .left,
// spacingAfter: 19
// ),
// DrawerText(
// font: Fonts.Mulish.regular.font(size: 16.0),
// text: subtitle,
// color: Asset.neutralBody.color,
// alignment: .left,
// lineHeightMultiple: 1.1,
// spacingAfter: 39
// ),
// actionButton
// ])
//
// actionButton.action.receive(on: DispatchQueue.main)
// .sink {
// drawer.dismiss(animated: true) { [weak self] in
// guard let self = self else { return }
// self.drawerCancellables.removeAll()
// actionClosure()
// }
// }.store(in: &drawerCancellables)
//
// coordinator.toDrawer(drawer, from: self)
// }
}
import UIKit
import Shared
import Models
import XXModels
import MenuFeature
import ChatFeature
import Presentation
......@@ -16,7 +17,7 @@ public protocol ChatListCoordinating {
func toContact(_: Contact, from: UIViewController)
func toSingleChat(with: Contact, from: UIViewController)
func toDrawer(_: UIViewController, from: UIViewController)
func toGroupChat(with: GroupChatInfo, from: UIViewController)
func toGroupChat(with: GroupInfo, from: UIViewController)
}
public struct ChatListCoordinator: ChatListCoordinating {
......@@ -31,7 +32,7 @@ public struct ChatListCoordinator: ChatListCoordinating {
var contactsFactory: () -> UIViewController
var contactFactory: (Contact) -> UIViewController
var singleChatFactory: (Contact) -> UIViewController
var groupChatFactory: (GroupChatInfo) -> UIViewController
var groupChatFactory: (GroupInfo) -> UIViewController
var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController
public init(
......@@ -41,7 +42,7 @@ public struct ChatListCoordinator: ChatListCoordinating {
contactsFactory: @escaping () -> UIViewController,
contactFactory: @escaping (Contact) -> UIViewController,
singleChatFactory: @escaping (Contact) -> UIViewController,
groupChatFactory: @escaping (GroupChatInfo) -> UIViewController,
groupChatFactory: @escaping (GroupInfo) -> UIViewController,
sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController
) {
self.scanFactory = scanFactory
......@@ -81,7 +82,7 @@ public extension ChatListCoordinator {
pushPresenter.present(screen, from: parent)
}
func toGroupChat(with group: GroupChatInfo, from parent: UIViewController) {
func toGroupChat(with group: GroupInfo, from parent: UIViewController) {
let screen = groupChatFactory(group)
pushPresenter.present(screen, from: parent)
}
......
import Models
import Foundation
import DifferenceKit
enum Chat: Equatable, Differentiable, Hashable {
case group(GroupChatInfo)
case contact(SingleChatInfo)
var differenceIdentifier: Data {
switch self {
case .contact(let info):
return info.contact.userId
case .group(let info):
return info.group.groupId
}
}
var orderingDate: Date {
switch self {
case .group(let info):
if let lastMessage = info.lastMessage {
return Date.fromTimestamp(lastMessage.timestamp)
} else {
return info.group.createdAt
}
case .contact(let info):
guard let lastMessage = info.lastMessage else {
fatalError("Should have an E2E chat without a last message")
}
return Date.fromTimestamp(lastMessage.timestamp)
}
}
}
//import Models
//import XXModels
//import Foundation
//import DifferenceKit
//
//enum Chat: Equatable, Differentiable, Hashable {
// case group(GroupChatInfo)
// case contact(SingleChatInfo)
//
// var differenceIdentifier: Data {
// switch self {
// case .contact(let info):
// return info.contact.userId
// case .group(let info):
// return info.group.groupId
// }
// }
//
// var orderingDate: Date {
// switch self {
// case .group(let info):
// if let lastMessage = info.lastMessage {
// return Date.fromTimestamp(lastMessage.timestamp)
// } else {
// return info.group.createdAt
// }
// case .contact(let info):
// guard let lastMessage = info.lastMessage else {
// fatalError("Should have an E2E chat without a last message")
// }
//
// return Date.fromTimestamp(lastMessage.timestamp)
// }
// }
//}
......@@ -3,6 +3,7 @@ import UIKit
import Shared
import Models
import Combine
import XXModels
import Defaults
import Integration
import DependencyInjection
......@@ -12,13 +13,13 @@ enum SearchSection {
case connections
}
enum SearchItem: Equatable, Hashable {
case chat(Chat)
case connection(Contact)
}
//enum SearchItem: Equatable, Hashable {
// case chat(Chat)
// case connection(Contact)
//}
typealias RecentsSnapshot = NSDiffableDataSourceSnapshot<SectionId, Contact>
typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem>
//typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem>
final class ChatListViewModel {
@Dependency private var session: SessionType
......@@ -27,96 +28,96 @@ final class ChatListViewModel {
session.isOnline
}
var chatsPublisher: AnyPublisher<[Chat], Never> {
chatsSubject.eraseToAnyPublisher()
}
// var chatsPublisher: AnyPublisher<[Chat], Never> {
// chatsSubject.eraseToAnyPublisher()
// }
var hudPublisher: AnyPublisher<HUDStatus, Never> {
hudSubject.eraseToAnyPublisher()
}
var recentsPublisher: AnyPublisher<RecentsSnapshot, Never> {
session.contacts(.isRecent).map {
let section = SectionId()
var snapshot = RecentsSnapshot()
snapshot.appendSections([section])
snapshot.appendItems($0, toSection: section)
return snapshot
}.eraseToAnyPublisher()
}
var searchPublisher: AnyPublisher<SearchSnapshot, Never> {
Publishers.CombineLatest3(
session.contacts(.all),
chatsPublisher,
searchSubject
.removeDuplicates()
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
)
.map { (contacts, chats, query) in
let connectionItems = contacts.filter {
let username = $0.username.lowercased().contains(query.lowercased())
let nickname = $0.nickname?.lowercased().contains(query.lowercased()) ?? false
return username || nickname
}.map(SearchItem.connection)
let chatItems = chats.filter {
switch $0 {
case .contact(let info):
let username = info.contact.username.lowercased().contains(query.lowercased())
let nickname = info.contact.nickname?.lowercased().contains(query.lowercased()) ?? false
let lastMessage = info.lastMessage?.payload.text.lowercased().contains(query.lowercased()) ?? false
return username || nickname || lastMessage
case .group(let info):
let name = info.group.name.lowercased().contains(query.lowercased())
let last = info.lastMessage?.payload.text.lowercased().contains(query.lowercased()) ?? false
return name || last
}
}.map(SearchItem.chat)
var snapshot = SearchSnapshot()
if connectionItems.count > 0 {
snapshot.appendSections([.connections])
snapshot.appendItems(connectionItems, toSection: .connections)
}
if chatItems.count > 0 {
snapshot.appendSections([.chats])
snapshot.appendItems(chatItems, toSection: .chats)
}
return snapshot
}.eraseToAnyPublisher()
}
var badgeCountPublisher: AnyPublisher<Int, Never> {
Publishers.CombineLatest(
session.contacts(.received),
session.groups(.pending)
)
.map { $0.0.count + $0.1.count }
.eraseToAnyPublisher()
}
// var recentsPublisher: AnyPublisher<RecentsSnapshot, Never> {
// session.contacts(.isRecent).map {
// let section = SectionId()
// var snapshot = RecentsSnapshot()
// snapshot.appendSections([section])
// snapshot.appendItems($0, toSection: section)
// return snapshot
// }.eraseToAnyPublisher()
// }
// var searchPublisher: AnyPublisher<SearchSnapshot, Never> {
// Publishers.CombineLatest3(
// session.contacts(.all),
// chatsPublisher,
// searchSubject
// .removeDuplicates()
// .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
// .eraseToAnyPublisher()
// )
// .map { (contacts, chats, query) in
// let connectionItems = contacts.filter {
// let username = $0.username.lowercased().contains(query.lowercased())
// let nickname = $0.nickname?.lowercased().contains(query.lowercased()) ?? false
// return username || nickname
// }.map(SearchItem.connection)
//
// let chatItems = chats.filter {
// switch $0 {
// case .contact(let info):
// let username = info.contact.username.lowercased().contains(query.lowercased())
// let nickname = info.contact.nickname?.lowercased().contains(query.lowercased()) ?? false
// let lastMessage = info.lastMessage?.payload.text.lowercased().contains(query.lowercased()) ?? false
// return username || nickname || lastMessage
//
// case .group(let info):
// let name = info.group.name.lowercased().contains(query.lowercased())
// let last = info.lastMessage?.payload.text.lowercased().contains(query.lowercased()) ?? false
// return name || last
// }
// }.map(SearchItem.chat)
//
// var snapshot = SearchSnapshot()
//
// if connectionItems.count > 0 {
// snapshot.appendSections([.connections])
// snapshot.appendItems(connectionItems, toSection: .connections)
// }
//
// if chatItems.count > 0 {
// snapshot.appendSections([.chats])
// snapshot.appendItems(chatItems, toSection: .chats)
// }
//
// return snapshot
// }.eraseToAnyPublisher()
// }
//
// var badgeCountPublisher: AnyPublisher<Int, Never> {
// Publishers.CombineLatest(
// session.contacts(.received),
// session.groups(.pending)
// )
// .map { $0.0.count + $0.1.count }
// .eraseToAnyPublisher()
// }
private var cancellables = Set<AnyCancellable>()
private let searchSubject = CurrentValueSubject<String, Never>("")
private let chatsSubject = CurrentValueSubject<[Chat], Never>([])
// private let chatsSubject = CurrentValueSubject<[Chat], Never>([])
private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none)
init() {
Publishers.CombineLatest(
session.singleChats(.all),
session.groupChats(.accepted)
).map {
let groups = $0.1.map(Chat.group)
let chats = $0.0.map(Chat.contact)
return (chats + groups).sorted { $0.orderingDate > $1.orderingDate }
}
.sink { [unowned self] in chatsSubject.send($0) }
.store(in: &cancellables)
// Publishers.CombineLatest(
// session.singleChats(.all),
// session.groupChats(.accepted)
// ).map {
// let groups = $0.1.map(Chat.group)
// let chats = $0.0.map(Chat.contact)
// return (chats + groups).sorted { $0.orderingDate > $1.orderingDate }
// }
// .sink { [unowned self] in chatsSubject.send($0) }
// .store(in: &cancellables)
}
func updateSearch(query: String) {
......@@ -128,7 +129,7 @@ final class ChatListViewModel {
do {
try session.leave(group: group)
session.deleteAll(from: group)
try session.dbManager.deleteMessages(.init(chat: .group(group.id)))
hudSubject.send(.none)
} catch {
hudSubject.send(.error(.init(with: error)))
......@@ -136,6 +137,6 @@ final class ChatListViewModel {
}
func clear(_ contact: Contact) {
session.deleteAll(from: contact)
_ = try? session.dbManager.deleteMessages(.init(chat: .direct(session.myId, contact.id)))
}
}
......@@ -47,8 +47,9 @@ final class ContactListTableController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: SmallAvatarAndTitleCell = tableView.dequeueReusableCell(forIndexPath: indexPath)
let contact = sections[indexPath.section][indexPath.row]
cell.titleLabel.text = contact.nickname ?? contact.username
cell.avatarView.setupProfile(title: contact.nickname ?? contact.username, image: contact.photo, size: .medium)
let name = (contact.nickname ?? contact.username) ?? "Fetching username..."
cell.titleLabel.text = name
cell.avatarView.setupProfile(title: name, image: contact.photo, size: .medium)
return cell
}
......
......@@ -74,7 +74,7 @@ public final class CreateGroupController: UIViewController {
) { [weak viewModel] collectionView, indexPath, contact in
let cell: CreateGroupCollectionCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
cell.setup(title: contact.nickname ?? contact.username, image: contact.photo)
cell.setup(title: contact.nickname ?? contact.username!, image: contact.photo)
cell.didTapRemove = { viewModel?.didSelect(contact: contact) }
return cell
......@@ -85,7 +85,7 @@ public final class CreateGroupController: UIViewController {
) { [weak self] tableView, indexPath, contact in
let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: SmallAvatarAndTitleCell.self)
cell.titleLabel.text = contact.nickname ?? contact.username
cell.avatarView.setupProfile(title: contact.nickname ?? contact.username, image: contact.photo, size: .medium)
cell.avatarView.setupProfile(title: contact.nickname ?? contact.username!, image: contact.photo, size: .medium)
if let selectedElements = self?.selectedElements, selectedElements.contains(contact) {
tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none)
......@@ -183,3 +183,9 @@ extension CreateGroupController: UITableViewDelegate {
}
}
}
extension Contact: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment