From fe2b14eceecbc1f892df66ddc36c9dca7ef3651e Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Fri, 28 Aug 2026 14:51:57 +0700 Subject: [PATCH 1/4] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D1=83=20=D0=9A=D0=BE=D0=BF?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D1=82=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=20=D0=B2=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=BA=D1=81?= =?UTF-8?q?=D1=82=D0=BD=D0=BE=D0=BC=20=D0=BC=D0=B5=D0=BD=D1=8E=20=D1=81?= =?UTF-8?q?=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=20#13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Shared/UI/Components/MessageBubble.swift | 156 +++++++++++------- 1 file changed, 96 insertions(+), 60 deletions(-) diff --git a/Sources/Shared/UI/Components/MessageBubble.swift b/Sources/Shared/UI/Components/MessageBubble.swift index 495a87a..0d2dd17 100644 --- a/Sources/Shared/UI/Components/MessageBubble.swift +++ b/Sources/Shared/UI/Components/MessageBubble.swift @@ -1,9 +1,9 @@ import SwiftUI #if os(iOS) -import UIKit + import UIKit #elseif os(macOS) -import AppKit + import AppKit #endif @MainActor @@ -71,6 +71,18 @@ struct MessageBubble: View { Label("Переслать", systemImage: "arrowshape.turn.up.right") } } + if message.hasText { + Button(action: { + #if os(macOS) + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(message.text, forType: .string) + #else + UIPasteboard.general.string = message.text + #endif + }) { + Label("Копировать текст", systemImage: "doc.on.doc") + } + } if model.canRetryMediaMessage(message) { Button(action: onRetry) { Label("Повторить отправку", systemImage: "arrow.clockwise") @@ -142,16 +154,20 @@ struct MessageBubble: View { } private var replySwipeIcon: some View { - Image(systemName: replySwipeArmed - ? "arrowshape.turn.up.left.fill" - : "arrowshape.turn.up.left") - .font(.system(size: 19, weight: .semibold)) - .foregroundStyle(Color.accentColor) - .scaleEffect( - CGFloat(0.8) + CGFloat(0.2) * MessageReplySwipePolicy.progress(for: replySwipeOffset) - ) - .opacity(Double( - CGFloat(0.35) + CGFloat(0.65) * MessageReplySwipePolicy.progress(for: replySwipeOffset) + Image( + systemName: replySwipeArmed + ? "arrowshape.turn.up.left.fill" + : "arrowshape.turn.up.left" + ) + .font(.system(size: 19, weight: .semibold)) + .foregroundStyle(Color.accentColor) + .scaleEffect( + CGFloat(0.8) + CGFloat(0.2) * MessageReplySwipePolicy.progress(for: replySwipeOffset) + ) + .opacity( + Double( + CGFloat(0.35) + CGFloat(0.65) + * MessageReplySwipePolicy.progress(for: replySwipeOffset) )) } @@ -192,9 +208,9 @@ struct MessageBubble: View { } private func replySwipeFeedback() { -#if os(iOS) - UIImpactFeedbackGenerator(style: .light).impactOccurred() -#endif + #if os(iOS) + UIImpactFeedbackGenerator(style: .light).impactOccurred() + #endif } private var selectionAccessibilityLabel: String { @@ -232,7 +248,8 @@ struct MessageBubble: View { .contentShape(Rectangle()) .onTapGesture { guard let targetID = message.replyToID, - message.kind == .text || message.kind == .system else { return } + message.kind == .text || message.kind == .system + else { return } onReplyTap(targetID) } } @@ -246,8 +263,11 @@ struct MessageBubble: View { if message.security != .omemo || message.delivery == .failed { HStack(spacing: 4) { if message.security != .omemo { - Image(systemName: message.security == .plaintext ? "lock.open" : "exclamationmark.shield") - .foregroundStyle(message.security == .plaintext ? .orange : .red) + Image( + systemName: message.security == .plaintext + ? "lock.open" : "exclamationmark.shield" + ) + .foregroundStyle(message.security == .plaintext ? .orange : .red) } if message.delivery == .failed { Image(systemName: "exclamationmark.circle.fill") @@ -262,8 +282,11 @@ struct MessageBubble: View { private var standardMetadata: some View { HStack(spacing: 4) { if message.callHistory == nil, message.security != .omemo { - Image(systemName: message.security == .plaintext ? "lock.open" : "exclamationmark.shield") - .foregroundStyle(message.security == .plaintext ? .orange : .red) + Image( + systemName: message.security == .plaintext + ? "lock.open" : "exclamationmark.shield" + ) + .foregroundStyle(message.security == .plaintext ? .orange : .red) } if message.editedAt != nil { Text("изм.") @@ -290,7 +313,9 @@ struct MessageBubble: View { .font(.subheadline) .padding(.horizontal, 13) .padding(.vertical, 9) - .foregroundStyle(message.direction == .outgoing ? Color.white.opacity(0.86) : Color.secondary) + .foregroundStyle( + message.direction == .outgoing ? Color.white.opacity(0.86) : Color.secondary + ) .background(bubbleBackground) .clipShape(messageBubbleShape) } else { @@ -370,16 +395,20 @@ struct MessageBubble: View { HStack(spacing: 12) { ZStack { Circle() - .fill(callAccentColor(for: history).opacity( - message.direction == .outgoing ? 0.28 : 0.14 - )) + .fill( + callAccentColor(for: history).opacity( + message.direction == .outgoing ? 0.28 : 0.14 + )) Image(systemName: history.isVideo ? "video.fill" : "phone.fill") .font(.system(size: 17, weight: .semibold)) - Image(systemName: message.direction == .outgoing ? "arrow.up.right" : "arrow.down.left") - .font(.system(size: 8, weight: .heavy)) - .padding(3) - .background(Circle().fill(callDirectionBadgeBackground)) - .offset(x: 14, y: 14) + Image( + systemName: message.direction == .outgoing + ? "arrow.up.right" : "arrow.down.left" + ) + .font(.system(size: 8, weight: .heavy)) + .padding(3) + .background(Circle().fill(callDirectionBadgeBackground)) + .offset(x: 14, y: 14) } .frame(width: 42, height: 42) .foregroundStyle(callIconForeground(for: history)) @@ -488,9 +517,10 @@ struct MessageBubble: View { .padding(.bottom, -2) .task(id: replyTarget?.remoteAttachmentURL) { if let replyTarget, - replyTarget.kind == .photo + replyTarget.kind == .photo || replyTarget.kind == .video - || replyTarget.kind == .videoNote { + || replyTarget.kind == .videoNote + { await model.prepareMediaPreview(replyTarget) } } @@ -561,25 +591,25 @@ struct MessageBubble: View { @ViewBuilder private func replyThumbnailImage(for target: ChatMessage) -> some View { if let data = model.mediaThumbnail(for: target) { -#if os(iOS) - if let image = ChatMediaImageCache.image(for: target, data: data) { - Image(uiImage: image) - .resizable() - .scaledToFill() - } else { - Image(systemName: replyMediaIcon(for: target.kind)) - .foregroundStyle(compactReplyColor) - } -#elseif os(macOS) - if let image = ChatMediaImageCache.image(for: target, data: data) { - Image(nsImage: image) - .resizable() - .scaledToFill() - } else { - Image(systemName: replyMediaIcon(for: target.kind)) - .foregroundStyle(compactReplyColor) - } -#endif + #if os(iOS) + if let image = ChatMediaImageCache.image(for: target, data: data) { + Image(uiImage: image) + .resizable() + .scaledToFill() + } else { + Image(systemName: replyMediaIcon(for: target.kind)) + .foregroundStyle(compactReplyColor) + } + #elseif os(macOS) + if let image = ChatMediaImageCache.image(for: target, data: data) { + Image(nsImage: image) + .resizable() + .scaledToFill() + } else { + Image(systemName: replyMediaIcon(for: target.kind)) + .foregroundStyle(compactReplyColor) + } + #endif } else { Image(systemName: replyMediaIcon(for: target.kind)) .font(.system(size: 12, weight: .semibold)) @@ -611,7 +641,9 @@ struct MessageBubble: View { ZStack { Circle() .fill(.white.opacity(message.direction == .outgoing ? 0.22 : 0.12)) - .frame(width: message.kind == .videoNote ? 58 : 38, height: message.kind == .videoNote ? 58 : 38) + .frame( + width: message.kind == .videoNote ? 58 : 38, + height: message.kind == .videoNote ? 58 : 38) Image(systemName: mediaIcon) .font(.system(size: message.kind == .videoNote ? 24 : 17)) } @@ -649,7 +681,8 @@ struct MessageBubble: View { parts.append(String(format: "%d:%02d", seconds / 60, seconds % 60)) } if let byteCount = message.byteCount { - parts.append(ByteCountFormatter.string(fromByteCount: Int64(byteCount), countStyle: .file)) + parts.append( + ByteCountFormatter.string(fromByteCount: Int64(byteCount), countStyle: .file)) } return parts.isEmpty ? "Нажмите для просмотра" : parts.joined(separator: " · ") } @@ -684,7 +717,10 @@ struct MessageBubble: View { private var bubbleBackground: some View { if message.direction == .outgoing { LinearGradient( - colors: [Color(red: 0.13, green: 0.51, blue: 0.94), Color(red: 0.12, green: 0.63, blue: 0.94)], + colors: [ + Color(red: 0.13, green: 0.51, blue: 0.94), + Color(red: 0.12, green: 0.63, blue: 0.94), + ], startPoint: .topLeading, endPoint: .bottomTrailing ) @@ -722,13 +758,13 @@ struct MessageBubble: View { } } -private extension View { +extension View { @ViewBuilder - func lumaTextSelection() -> some View { -#if os(macOS) - textSelection(.enabled) -#else - self -#endif + fileprivate func lumaTextSelection() -> some View { + #if os(macOS) + textSelection(.enabled) + #else + self + #endif } } -- 2.54.0 From 4c16644b6960c16eb45b9f3ca287267b445e71c4 Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Fri, 28 Aug 2026 15:10:37 +0700 Subject: [PATCH 2/4] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20gasText=20=D0=B2=20ChatMessage=20#13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Shared/Models/ChatMessage.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Sources/Shared/Models/ChatMessage.swift b/Sources/Shared/Models/ChatMessage.swift index 954981b..b9e15e8 100644 --- a/Sources/Shared/Models/ChatMessage.swift +++ b/Sources/Shared/Models/ChatMessage.swift @@ -167,7 +167,8 @@ final class ChatMessage { var quotePreview: String { if isRetracted { return "Сообщение удалено" } let value = kind == .text ? body : previewText - let normalized = value + let normalized = + value .replacingOccurrences(of: "\n", with: " ") .trimmingCharacters(in: .whitespacesAndNewlines) guard normalized.count > 140 else { return normalized } @@ -264,4 +265,15 @@ final class ChatMessage { let bare = value[.. Date: Fri, 28 Aug 2026 15:17:52 +0700 Subject: [PATCH 3/4] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=20mes?= =?UTF-8?q?sage.text->meesage.body=20#13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Shared/UI/Components/MessageBubble.swift | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Sources/Shared/UI/Components/MessageBubble.swift b/Sources/Shared/UI/Components/MessageBubble.swift index 0d2dd17..1625cc6 100644 --- a/Sources/Shared/UI/Components/MessageBubble.swift +++ b/Sources/Shared/UI/Components/MessageBubble.swift @@ -72,14 +72,7 @@ struct MessageBubble: View { } } if message.hasText { - Button(action: { - #if os(macOS) - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(message.text, forType: .string) - #else - UIPasteboard.general.string = message.text - #endif - }) { + Button(action: copyText(message.body)) { Label("Копировать текст", systemImage: "doc.on.doc") } } @@ -117,6 +110,15 @@ struct MessageBubble: View { } } + private func copyText(body: String) { + #if os(macOS) + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(message.body, forType: .string) + #else + UIPasteboard.general.string = message.body + #endif + } + private var messageContainer: some View { HStack(spacing: 2) { if isSelectionMode { -- 2.54.0 From 233eb7e189acece687aff29fb76aaca6ebdb3d8e Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Fri, 28 Aug 2026 15:21:22 +0700 Subject: [PATCH 4/4] =?UTF-8?q?=D0=BF=D0=BE=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=81=D0=B8=D0=BD=D1=82=D0=B0=D0=BA=D1=81=D0=B8=D1=87?= =?UTF-8?q?=D0=B5=D1=81=D0=BA=D1=83=D1=8E=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA?= =?UTF-8?q?=D1=83=20#13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Shared/UI/Components/MessageBubble.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Shared/UI/Components/MessageBubble.swift b/Sources/Shared/UI/Components/MessageBubble.swift index 1625cc6..e2ed8f6 100644 --- a/Sources/Shared/UI/Components/MessageBubble.swift +++ b/Sources/Shared/UI/Components/MessageBubble.swift @@ -72,7 +72,7 @@ struct MessageBubble: View { } } if message.hasText { - Button(action: copyText(message.body)) { + Button(action: copyText) { Label("Копировать текст", systemImage: "doc.on.doc") } } @@ -110,7 +110,7 @@ struct MessageBubble: View { } } - private func copyText(body: String) { + private func copyText() { #if os(macOS) NSPasteboard.general.clearContents() NSPasteboard.general.setString(message.body, forType: .string) -- 2.54.0