Compare commits

...

4 Commits

Author SHA1 Message Date
a66eb04489 patch show quick action 2025-12-13 04:45:38 +03:00
feed384cf1 update contact 2025-12-13 04:17:36 +03:00
5601f475c8 patch contacts 2025-12-13 04:08:55 +03:00
4e24f468b8 delete refresh in contact 2025-12-13 03:57:57 +03:00
2 changed files with 107 additions and 25 deletions

View File

@ -676,9 +676,13 @@ struct MessageProfileView: View {
formatted
)
}
private var shouldShowRelationshipQuickActions: Bool {
if isDeletedUser { return false }
guard let relationship = currentChatProfile?.relationship else { return false }
// показываем только если НЕ в контактах
return !relationship.isTargetInContactsOfCurrentUser
}

View File

@ -13,9 +13,13 @@ struct ContactsTab: View {
@State private var creatingChatForContactId: UUID?
@State private var pendingChatItem: PrivateChatListItem?
@State private var isPendingChatActive = false
@State private var contactAvatars: [UUID: AvatarInfo] = [:]
@State private var avatarLoadedIds: Set<UUID> = []
@State private var avatarLoadingIds: Set<UUID> = []
private let contactsService = ContactsService()
private let chatService = ChatService()
private let profileService = ProfileService()
private let pageSize = 25
private var currentUserId: String? {
@ -42,11 +46,17 @@ struct ContactsTab: View {
Button {
openChat(for: contact)
} label: {
ContactRow(contact: contact, isLoading: creatingChatForContactId == contact.id)
ContactRow(
contact: contact,
avatarInfo: contactAvatars[contact.id],
currentUserId: currentUserId,
isLoading: creatingChatForContactId == contact.id
)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.disabled(contact.isDeleted || creatingChatForContactId == contact.id)
.disabled(creatingChatForContactId == contact.id)
// .disabled(contact.isDeleted || creatingChatForContactId == contact.id)
.contextMenu {
Button {
handleContactAction(.edit, for: contact)
@ -80,6 +90,7 @@ struct ContactsTab: View {
}
.listRowInsets(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12))
.onAppear {
loadAvatarIfNeeded(for: contact)
if index >= contacts.count - 5 {
Task {
await loadContacts(reset: false)
@ -101,9 +112,6 @@ struct ContactsTab: View {
.task {
await loadContacts(reset: false)
}
.refreshable {
await refreshContacts()
}
.alert(item: $activeAlert) { alert in
switch alert {
case .error(_, let message):
@ -270,7 +278,6 @@ struct ContactsTab: View {
private func openChat(for contact: Contact) {
guard creatingChatForContactId == nil else { return }
guard !contact.isDeleted else { return }
creatingChatForContactId = contact.id
@ -344,35 +351,56 @@ struct ContactsTab: View {
return NSLocalizedString("Произошла неизвестная ошибка. Попробуйте позже.", comment: "Chat creation unknown error")
}
private func loadAvatarIfNeeded(for contact: Contact) {
guard !contact.isDeleted else { return }
let contactId = contact.id
if avatarLoadedIds.contains(contactId) || avatarLoadingIds.contains(contactId) {
return
}
avatarLoadingIds.insert(contactId)
Task {
do {
let profile = try await profileService.fetchProfile(userId: contactId)
await MainActor.run {
if let info = profile.avatars?.current {
contactAvatars[contactId] = info
}
avatarLoadedIds.insert(contactId)
avatarLoadingIds.remove(contactId)
}
} catch {
if AppConfig.DEBUG {
print("[ContactsTab] load avatar failed for \(contactId): \(error)")
}
await MainActor.run {
avatarLoadingIds.remove(contactId)
}
}
}
}
}
private struct ContactRow: View {
let contact: Contact
let avatarInfo: AvatarInfo?
let currentUserId: String?
let isLoading: Bool
init(contact: Contact, isLoading: Bool = false) {
private let avatarSize: CGFloat = 40
init(contact: Contact, avatarInfo: AvatarInfo? = nil, currentUserId: String? = nil, isLoading: Bool = false) {
self.contact = contact
self.avatarInfo = avatarInfo
self.currentUserId = currentUserId
self.isLoading = isLoading
}
var body: some View {
HStack(alignment: .top, spacing: 10) {
Circle()
.fill(contact.isDeleted ? Color(.systemGray5) : Color.accentColor.opacity(0.15))
.frame(width: 40, height: 40)
.overlay(
Group {
if contact.isDeleted {
Image(systemName: "person.slash")
.font(.headline)
.foregroundColor(Color(.systemGray2))
} else {
Text(contact.initials)
.font(.headline)
.foregroundColor(.accentColor)
}
}
)
HStack(alignment: .top, spacing: 12) {
avatarView
VStack(alignment: .leading, spacing: 3) {
HStack(alignment: .firstTextBaseline) {
@ -414,6 +442,56 @@ private struct ContactRow: View {
.padding(.vertical, 6)
}
@ViewBuilder
private var avatarView: some View {
if let fileId = avatarInfo?.fileId,
let url = avatarURL(for: fileId),
let currentUserId {
CachedAvatarView(url: url, fileId: fileId, userId: currentUserId) {
placeholderAvatar
}
.aspectRatio(contentMode: .fill)
.frame(width: avatarSize, height: avatarSize)
.clipShape(Circle())
} else {
placeholderAvatar
}
}
private func avatarURL(for fileId: String) -> URL? {
let userId = contact.id.uuidString
let path = "\(AppConfig.API_SERVER)/v1/storage/download/avatar/\(userId)?file_id=\(fileId)"
return URL(string: path)
}
private var placeholderAvatar: some View {
Circle()
.fill(avatarBackgroundColor)
.frame(width: avatarSize, height: avatarSize)
.overlay(
Group {
if contact.isDeleted {
Image(systemName: "person.slash")
.symbolRenderingMode(.hierarchical)
.font(.system(size: avatarSize * 0.45, weight: .semibold))
.foregroundColor(avatarTextColor)
} else {
Text(contact.initials)
.font(.system(size: avatarSize * 0.5, weight: .semibold))
.foregroundColor(avatarTextColor)
}
}
)
}
private var avatarBackgroundColor: Color {
contact.isDeleted ? Color(.systemGray5) : Color.accentColor.opacity(0.15)
}
private var avatarTextColor: Color {
contact.isDeleted ? Color.accentColor : Color.accentColor
}
private var friendCodeBadge: some View {
Text(NSLocalizedString("Код дружбы", comment: "Friend code badge"))
.font(.caption2.weight(.medium))