diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/AudioBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/AudioBubbleView.swift index 8ff6757aca..c4bafa94c6 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/AudioBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/AudioBubbleView.swift @@ -13,10 +13,8 @@ import SwiftUI struct AudioBubbleView: View { let audio: AudioVisual let caption: String - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? - let sendingState: SendingState @Environment(ChatHistoryStore.self) private var store @Environment(\.bubbleMetrics) private var metrics @@ -73,13 +71,12 @@ struct AudioBubbleView: View { .font(.caption) .fixedSize(horizontal: false, vertical: true) } - timeRow } .padding(.horizontal, 8) .padding(.vertical, 6) - .frame(maxWidth: metrics.bubbleMaxWidth, alignment: .leading) + .frame(minWidth: BubbleShape.minSize, maxWidth: metrics.bubbleMaxWidth, minHeight: BubbleShape.minSize, alignment: .leading) .background( - RoundedRectangle(cornerRadius: 10) + RoundedRectangle(cornerRadius: BubbleShape.cornerRadius) .fill(style.fill) ) .foregroundStyle(style.content) @@ -118,28 +115,6 @@ struct AudioBubbleView: View { .shadow(radius: audio.albumArt != nil ? 1 : 0) } - // Time + send-state inline at the bottom-right inside the chrome, matching the - // text-bubble convention (and VoiceNoteBubbleView). - private var timeRow: some View { - HStack(spacing: 2) { - Spacer(minLength: 0) - Text(time) - .font(.system(size: 8)) - .foregroundStyle(style.secondary) - if isOutgoing { sendStateGlyph } - } - } - - @ViewBuilder - private var sendStateGlyph: some View { - switch sendingState { - case .sent: EmptyView() - case .pending: Image(systemName: "clock").font(.system(size: 8)) - .foregroundStyle(Color.white.opacity(0.7)) - case .failed: Image(systemName: "exclamationmark.circle.fill").font(.system(size: 8)) - .foregroundStyle(.red) - } - } } #if DEBUG @@ -186,8 +161,8 @@ private func previewAudio( #Preview("Audio — incoming, idle") { AudioBubbleView( audio: previewAudio(), caption: "", - time: "12:34", isOutgoing: false, - replyHeader: nil, sendingState: .sent + isOutgoing: false, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -196,8 +171,8 @@ private func previewAudio( #Preview("Audio — outgoing, idle") { AudioBubbleView( audio: previewAudio(id: 2), caption: "", - time: "12:35", isOutgoing: true, - replyHeader: nil, sendingState: .sent + isOutgoing: true, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -207,8 +182,8 @@ private func previewAudio( AudioBubbleView( audio: previewAudio(id: 3, title: "A Very Long Track Title That Will Truncate", performer: "Some Long Performer Name"), caption: "", - time: "12:36", isOutgoing: false, - replyHeader: nil, sendingState: .sent + isOutgoing: false, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -218,8 +193,8 @@ private func previewAudio( AudioBubbleView( audio: previewAudio(id: 4, title: "Untitled", performer: ""), caption: "", - time: "12:37", isOutgoing: false, - replyHeader: nil, sendingState: .sent + isOutgoing: false, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -229,8 +204,8 @@ private func previewAudio( AudioBubbleView( audio: previewAudio(id: 5), caption: "this song goes hard", - time: "12:38", isOutgoing: false, - replyHeader: nil, sendingState: .sent + isOutgoing: false, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -239,13 +214,12 @@ private func previewAudio( #Preview("Audio — incoming with reply") { AudioBubbleView( audio: previewAudio(id: 6), caption: "", - time: "12:39", isOutgoing: false, + isOutgoing: false, replyHeader: ReplyHeader( senderName: "Bob", snippet: "anchor message earlier in the chat", minithumbnail: nil, isOutgoing: false - ), - sendingState: .sent + ) ) .bubblePreview() .environment(previewStore()) diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/BubbleShape.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/BubbleShape.swift new file mode 100644 index 0000000000..70030dd712 --- /dev/null +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/BubbleShape.swift @@ -0,0 +1,13 @@ +import CoreGraphics + +/// Single source of truth for message-bubble geometry. Every bubble surface +/// (text, voice, audio, document, poll, photo, video, map) clips/fills to this +/// radius; fill-chrome bubbles also enforce the minimum so a one-word message +/// renders as a 28×28 pill (radius == half-size → circular ends). +enum BubbleShape { + static let cornerRadius: CGFloat = 14 + /// Minimum bubble edge (min-width and min-height). Equals `2 × cornerRadius` + /// so a one-word fill-chrome bubble renders as a circle/pill rather than a + /// rounded-rect with concave-looking corners. + static let minSize: CGFloat = 28 +} diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/DocumentBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/DocumentBubbleView.swift index b5e9fb1b6c..b056d22dd6 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/DocumentBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/DocumentBubbleView.swift @@ -1,11 +1,10 @@ import SwiftUI /// Renders one generic-document bubble: BubbleStyle chrome with a rounded-square doc icon, -/// filename, size, optional caption, and a time footer. Display-only (no download/open on +/// filename, size, optional caption. Display-only (no download/open on /// watch in milestone #4). struct DocumentBubbleView: View { let document: DocumentVisual - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? @@ -35,15 +34,11 @@ struct DocumentBubbleView: View { .font(.caption) .fixedSize(horizontal: false, vertical: true) } - HStack(spacing: 2) { - Spacer(minLength: 0) - Text(time).font(.system(size: 8)).foregroundStyle(style.secondary) - } } .padding(.horizontal, 8) .padding(.vertical, 6) - .frame(maxWidth: metrics.bubbleMaxWidth, alignment: .leading) - .background(RoundedRectangle(cornerRadius: 10).fill(style.fill)) + .frame(minWidth: BubbleShape.minSize, maxWidth: metrics.bubbleMaxWidth, minHeight: BubbleShape.minSize, alignment: .leading) + .background(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius).fill(style.fill)) .foregroundStyle(style.content) } @@ -61,7 +56,7 @@ struct DocumentBubbleView: View { DocumentBubbleView( document: DocumentVisual(documentFileId: 1, fileName: "report.pdf", sizeBytes: 2_400_000, localPath: nil, caption: ""), - time: "12:34", isOutgoing: false, replyHeader: nil + isOutgoing: false, replyHeader: nil ).bubblePreview() } @@ -69,7 +64,7 @@ struct DocumentBubbleView: View { DocumentBubbleView( document: DocumentVisual(documentFileId: 2, fileName: "quarterly-financial-summary-2026.xlsx", sizeBytes: 52_428_800, localPath: nil, caption: ""), - time: "12:35", isOutgoing: true, replyHeader: nil + isOutgoing: true, replyHeader: nil ).bubblePreview() } @@ -77,7 +72,7 @@ struct DocumentBubbleView: View { DocumentBubbleView( document: DocumentVisual(documentFileId: 3, fileName: "archive.zip", sizeBytes: 52_428_800, localPath: nil, caption: "here are the files"), - time: "12:36", isOutgoing: false, replyHeader: nil + isOutgoing: false, replyHeader: nil ).bubblePreview() } @@ -85,7 +80,7 @@ struct DocumentBubbleView: View { DocumentBubbleView( document: DocumentVisual(documentFileId: 4, fileName: "notes.txt", sizeBytes: 1024, localPath: nil, caption: ""), - time: "12:37", isOutgoing: false, + isOutgoing: false, replyHeader: ReplyHeader(senderName: "Bob", snippet: "the doc", minithumbnail: nil, isOutgoing: false) ).bubblePreview() } diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/LocationBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/LocationBubbleView.swift index 81ead594d2..faba8db478 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/LocationBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/LocationBubbleView.swift @@ -5,7 +5,7 @@ import UIKit /// Renders one location / venue bubble: a rounded static map image /// (`MKMapSnapshotter`) with a centered pin, a "LIVE" badge for live -/// locations, and an inside time footer. The snapshot renders in `.task(id:)`, +/// locations. The snapshot renders in `.task(id:)`, /// keyed by the cache key, so live-location coordinate changes re-render /// automatically. /// @@ -17,7 +17,6 @@ import UIKit /// Tap builds an `MKMapItem` and hands the coordinate to the system Maps app. struct LocationBubbleView: View { let location: LocationVisual - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? @@ -91,7 +90,7 @@ struct LocationBubbleView: View { .frame(width: mapWidth, alignment: .leading) .background(style.fill) .foregroundStyle(style.content) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius)) } @ViewBuilder @@ -107,9 +106,8 @@ struct LocationBubbleView: View { private func mapView(roundedCorners: Bool) -> some View { mapImage .frame(width: mapWidth, height: mapHeight) - .clipShape(RoundedRectangle(cornerRadius: roundedCorners ? 12 : 0)) + .clipShape(RoundedRectangle(cornerRadius: roundedCorners ? BubbleShape.cornerRadius : 0)) .overlay { pin } - .overlay(alignment: .bottomTrailing) { timeFooter } .contentShape(Rectangle()) .onTapGesture { openInMaps() } } @@ -150,16 +148,6 @@ struct LocationBubbleView: View { } } - private var timeFooter: some View { - Text(time) - .font(.system(size: 8)) - .foregroundStyle(.white) - .padding(.horizontal, 4) - .padding(.vertical, 1) - .background(Capsule().fill(.black.opacity(0.5))) - .padding(4) - } - private func openInMaps() { let mapItem = MKMapItem( location: CLLocation(latitude: location.latitude, longitude: location.longitude), @@ -178,7 +166,7 @@ struct LocationBubbleView: View { title: nil, address: nil, isLive: false, heading: 0, isExpired: false, liveUpdatedAt: nil ), - time: "12:34", isOutgoing: false, replyHeader: nil + isOutgoing: false, replyHeader: nil ) .bubblePreview() } @@ -190,7 +178,7 @@ struct LocationBubbleView: View { title: "Eiffel Tower", address: "Champ de Mars, 75007 Paris", isLive: false, heading: 0, isExpired: false, liveUpdatedAt: nil ), - time: "12:35", isOutgoing: true, replyHeader: nil + isOutgoing: true, replyHeader: nil ) .bubblePreview() } @@ -202,7 +190,7 @@ struct LocationBubbleView: View { title: nil, address: nil, isLive: true, heading: 90, isExpired: false, liveUpdatedAt: Date().addingTimeInterval(-90) ), - time: "12:36", isOutgoing: false, replyHeader: nil + isOutgoing: false, replyHeader: nil ) .bubblePreview() } @@ -214,7 +202,7 @@ struct LocationBubbleView: View { title: nil, address: nil, isLive: true, heading: 0, isExpired: true, liveUpdatedAt: Date().addingTimeInterval(-7200) ), - time: "12:30", isOutgoing: false, replyHeader: nil + isOutgoing: false, replyHeader: nil ) .bubblePreview() } @@ -226,7 +214,7 @@ struct LocationBubbleView: View { title: nil, address: nil, isLive: false, heading: 0, isExpired: false, liveUpdatedAt: nil ), - time: "12:34", isOutgoing: false, + isOutgoing: false, replyHeader: ReplyHeader( senderName: "Bob", snippet: "where are you?", diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/MessageBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/MessageBubbleView.swift index ea0c79e6b1..aa1322c4e7 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/MessageBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/MessageBubbleView.swift @@ -24,7 +24,6 @@ struct MessageBubbleView: View { sticker: sticker, senderName: bubble.senderName, senderColorIndex: bubble.senderColorIndex, - time: bubble.time, isOutgoing: bubble.isOutgoing, replyHeader: bubble.replyHeader ) @@ -32,7 +31,6 @@ struct MessageBubbleView: View { PhotoBubbleView( photo: photo, caption: bubble.body, - time: bubble.time, isOutgoing: bubble.isOutgoing, replyHeader: bubble.replyHeader, onTap: onPhotoTap @@ -41,7 +39,6 @@ struct MessageBubbleView: View { VideoBubbleView( video: video, caption: bubble.body, - time: bubble.time, isOutgoing: bubble.isOutgoing, replyHeader: bubble.replyHeader, onTap: { onVideoTap(video) } @@ -49,7 +46,6 @@ struct MessageBubbleView: View { } else if let note = bubble.videoNote { VideoNoteBubbleView( note: note, - time: bubble.time, isOutgoing: bubble.isOutgoing, replyHeader: bubble.replyHeader, onTap: { onVideoNoteTap(note) } @@ -58,38 +54,31 @@ struct MessageBubbleView: View { VoiceNoteBubbleView( note: voice, caption: bubble.body, - time: bubble.time, isOutgoing: bubble.isOutgoing, - replyHeader: bubble.replyHeader, - sendingState: bubble.sendingState + replyHeader: bubble.replyHeader ) } else if let audio = bubble.audio { AudioBubbleView( audio: audio, caption: bubble.body, - time: bubble.time, isOutgoing: bubble.isOutgoing, - replyHeader: bubble.replyHeader, - sendingState: bubble.sendingState + replyHeader: bubble.replyHeader ) } else if let document = bubble.document { DocumentBubbleView( document: document, - time: bubble.time, isOutgoing: bubble.isOutgoing, replyHeader: bubble.replyHeader ) } else if let location = bubble.location { LocationBubbleView( location: location, - time: bubble.time, isOutgoing: bubble.isOutgoing, replyHeader: bubble.replyHeader ) } else if let poll = bubble.poll { PollBubbleView( poll: poll, - time: bubble.time, isOutgoing: bubble.isOutgoing, replyHeader: bubble.replyHeader, onVote: { onPollTap(bubble.messageId, poll) } @@ -101,14 +90,7 @@ struct MessageBubbleView: View { textBubbleContent } } - .overlay(alignment: .bottomLeading) { - if bubble.isUnreadOutgoing { - Circle() - .fill(Color.accentColor) - .frame(width: 7, height: 7) - .offset(x: -11) - } - } + .overlay(alignment: .bottomLeading) { statusIndicator } if !bubble.isOutgoing { Spacer(minLength: 16) } } .accessibilityIdentifier("bubble.\(bubble.messageId)") @@ -127,42 +109,42 @@ struct MessageBubbleView: View { if let header = bubble.replyHeader { ReplyHeaderView(header: header, style: BubbleStyle.resolve(isOutgoing: bubble.isOutgoing)) } - HStack(alignment: .lastTextBaseline, spacing: 4) { - Text(bubble.body) - .font(.caption) - .fixedSize(horizontal: false, vertical: true) - HStack(spacing: 2) { - Text(bubble.time) - .font(.system(size: 8)) - .foregroundStyle(style.secondary) - if bubble.isOutgoing { - sendStateGlyph - } - } - } + Text(bubble.body) + .font(.caption) + .fixedSize(horizontal: false, vertical: true) } - .padding(.horizontal, 8) + .padding(.horizontal, 9) .padding(.vertical, 4) + .frame(minWidth: BubbleShape.minSize, minHeight: BubbleShape.minSize) .background( - RoundedRectangle(cornerRadius: 10) + RoundedRectangle(cornerRadius: BubbleShape.cornerRadius) .fill(style.fill) ) .foregroundStyle(style.content) } + /// The single outgoing delivery indicator, in the bubble's leading gutter. + /// Glyphs sit slightly further left than the dot so they clear the bubble edge. @ViewBuilder - private var sendStateGlyph: some View { - switch bubble.sendingState { - case .sent: + private var statusIndicator: some View { + switch bubble.outgoingStatus { + case .none: EmptyView() + case .unread: + Circle() + .fill(Color.accentColor) + .frame(width: 7, height: 7) + .offset(x: -11) case .pending: Image(systemName: "clock") .font(.system(size: 8)) .foregroundStyle(Color.white.opacity(0.7)) + .offset(x: -13) case .failed: Image(systemName: "exclamationmark.circle.fill") .font(.system(size: 8)) .foregroundStyle(.red) + .offset(x: -13) } } } @@ -171,7 +153,6 @@ struct MessageBubbleView: View { private let previewSampleBubble = MessageBubble( messageId: 1, isOutgoing: false, senderName: "Alice", body: "this is a reply to text", - time: "12:34", photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil, sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: ReplyHeader( @@ -197,7 +178,6 @@ private let previewSampleBubble = MessageBubble( bubble: MessageBubble( messageId: 2, isOutgoing: true, senderName: nil, body: "ok", - time: "12:35", photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil, sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: ReplyHeader( @@ -219,7 +199,6 @@ private let previewSampleBubble = MessageBubble( bubble: MessageBubble( messageId: 3, isOutgoing: false, senderName: "Alice", body: "colored name above me", - time: "12:36", photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil, sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: nil, @@ -236,7 +215,7 @@ private let previewSampleBubble = MessageBubble( #Preview("Emoji only — incoming 1") { MessageBubbleView( bubble: MessageBubble( - messageId: 10, isOutgoing: false, senderName: nil, body: "🥰", time: "12:40", + messageId: 10, isOutgoing: false, senderName: nil, body: "🥰", photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil, sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: nil ), @@ -250,7 +229,6 @@ private let previewSampleBubble = MessageBubble( bubble: MessageBubble( messageId: 12, isOutgoing: true, senderName: nil, body: "delivered but not yet read", - time: "12:42", photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil, sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: nil, isUnreadOutgoing: true @@ -263,7 +241,7 @@ private let previewSampleBubble = MessageBubble( #Preview("Emoji only — outgoing 3") { MessageBubbleView( bubble: MessageBubble( - messageId: 11, isOutgoing: true, senderName: nil, body: "😀🎉🥰", time: "12:41", + messageId: 11, isOutgoing: true, senderName: nil, body: "😀🎉🥰", photo: nil, video: nil, videoNote: nil, voiceNote: nil, audio: nil, document: nil, sticker: nil, location: nil, poll: nil, sendingState: .sent, replyHeader: nil ), diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/MessageRow.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/MessageRow.swift index 57e6b44660..1a206b5ea7 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/MessageRow.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/MessageRow.swift @@ -24,8 +24,6 @@ struct MessageBubble: Equatable, Hashable { let senderName: String? /// Body text — for text bubbles, the message body; for photo / video bubbles, the caption (may be ""). let body: String - /// `HH:mm` of the message date in the user's time zone. - let time: String /// Photo metadata for `messagePhoto` content; nil for non-photo bubbles. let photo: PhotoVisual? /// Video metadata for `messageVideo` content; nil for non-video bubbles. @@ -57,6 +55,27 @@ struct MessageBubble: Equatable, Hashable { var senderColorIndex: Int? = nil /// True for a sent outgoing message the recipient hasn't read yet (`id > lastReadOutboxMessageId`). var isUnreadOutgoing: Bool = false + + /// The delivery-status indicator to render for this bubble. Outgoing only; + /// `isUnreadOutgoing` already encodes "sent, unread, not Saved Messages". + var outgoingStatus: OutgoingStatus { + guard isOutgoing else { return .none } + switch sendingState { + case .pending: return .pending + case .failed: return .failed + case .sent: return isUnreadOutgoing ? .unread : .none + } + } +} + +/// Collapses an outgoing message's delivery state into the single indicator the +/// chat UI shows at the bubble's bottom-leading corner. Incoming messages — and +/// outgoing messages that are sent and already read — show nothing. +enum OutgoingStatus: Equatable { + case none + case pending + case failed + case unread } struct ServiceLine: Equatable, Hashable { @@ -106,12 +125,6 @@ func messageRows( dayKeyFormatter.timeZone = calendar.timeZone dayKeyFormatter.dateFormat = "yyyy-MM-dd" - let timeFormatter = DateFormatter() - timeFormatter.calendar = calendar - timeFormatter.timeZone = calendar.timeZone - timeFormatter.locale = locale - timeFormatter.dateFormat = "HH:mm" - var rows: [MessageRow] = [] var lastDayKey: String? = nil var dividerPlaced = false @@ -148,7 +161,6 @@ func messageRows( isOutgoing: msg.isOutgoing, senderName: sender?.name, body: messageBody(msg.content), - time: timeFormatter.string(from: date), photo: photoVisual(for: msg.content, fileLocals: fileLocals), video: videoVisual(for: msg.content, fileLocals: fileLocals), videoNote: videoNoteVisual(for: msg.content, fileLocals: fileLocals), diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/PhotoBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/PhotoBubbleView.swift index 1826687921..63a51c47cd 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/PhotoBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/PhotoBubbleView.swift @@ -3,7 +3,7 @@ import UIKit /// Renders one photo bubble: aspect-fit image (full-resolution when downloaded, /// minithumbnail otherwise, gray placeholder if neither), optional caption below the -/// image, time stamp footer. +/// image. /// /// Drives viewport-based download via `.onScrollVisibilityChange` on the store — /// `.onAppear` would fire for every off-screen bubble too, because the enclosing @@ -13,7 +13,6 @@ import UIKit struct PhotoBubbleView: View { let photo: PhotoVisual let caption: String - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? let onTap: (PhotoVisual) -> Void @@ -45,9 +44,8 @@ struct PhotoBubbleView: View { private var bareImage: some View { imageView .frame(width: displaySize.width, height: displaySize.height) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .overlay(alignment: .bottomTrailing) { timePill } - .contentShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius)) + .contentShape(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius)) .onTapGesture { if photo.localPath != nil { onTap(photo) } } } @@ -69,25 +67,10 @@ struct PhotoBubbleView: View { .fixedSize(horizontal: false, vertical: true) .padding(.horizontal, 8) } - Text(time) - .font(.system(size: 8)) - .foregroundStyle(style.secondary) - .padding(.horizontal, 8) - .padding(.bottom, 4) } .frame(width: displaySize.width, alignment: .leading) .background(style.fill) - .clipShape(RoundedRectangle(cornerRadius: 12)) - } - - private var timePill: some View { - Text(time) - .font(.system(size: 8)) - .foregroundStyle(.white) - .padding(.horizontal, 4) - .padding(.vertical, 1) - .background(Capsule().fill(.black.opacity(0.5))) - .padding(4) + .clipShape(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius)) } @ViewBuilder @@ -154,7 +137,6 @@ private func photoPreviewStore() -> ChatHistoryStore { PhotoBubbleView( photo: PhotoVisual(fileId: 0, width: 800, height: 600, minithumbnail: nil, localPath: nil), caption: "look at this", - time: "12:34", isOutgoing: false, replyHeader: ReplyHeader( senderName: "Bob", @@ -172,7 +154,6 @@ private func photoPreviewStore() -> ChatHistoryStore { PhotoBubbleView( photo: PhotoVisual(fileId: 0, width: 800, height: 600, minithumbnail: nil, localPath: nil), caption: "", - time: "12:34", isOutgoing: false, replyHeader: nil, onTap: { _ in } @@ -185,7 +166,6 @@ private func photoPreviewStore() -> ChatHistoryStore { PhotoBubbleView( photo: PhotoVisual(fileId: 0, width: 800, height: 600, minithumbnail: nil, localPath: nil), caption: "Benji athlete today!", - time: "12:34", isOutgoing: false, replyHeader: nil, onTap: { _ in } diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/PollBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/PollBubbleView.swift index def98f8d13..44344fdc9e 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/PollBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/PollBubbleView.swift @@ -2,12 +2,11 @@ import SwiftUI /// Renders one poll / quiz bubble inside the standard gray-incoming / accent-outgoing /// chrome. Shows the question, option rows (with result bars + percentages once -/// `resultsVisible`), quiz correct/chosen markers + explanation, a voter-count + time -/// footer, and a "Vote" button when the poll is still actionable. Tapping Vote calls +/// `resultsVisible`), quiz correct/chosen markers + explanation, a voter-count footer, +/// and a "Vote" button when the poll is still actionable. Tapping Vote calls /// `onVote`, which opens the dedicated `PollVoteView`. struct PollBubbleView: View { let poll: PollVisual - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? let onVote: () -> Void @@ -63,18 +62,14 @@ struct PollBubbleView: View { .buttonStyle(.bordered) .controlSize(.mini) } - HStack(spacing: 4) { - Text(voterCountLabel) - Spacer(minLength: 0) - Text(time) - } - .font(.system(size: 9)) - .foregroundStyle(secondaryColor) + Text(voterCountLabel) + .font(.system(size: 9)) + .foregroundStyle(secondaryColor) } .padding(8) - .frame(maxWidth: maxWidth, alignment: .leading) + .frame(minWidth: BubbleShape.minSize, maxWidth: maxWidth, minHeight: BubbleShape.minSize, alignment: .leading) .background( - RoundedRectangle(cornerRadius: 12) + RoundedRectangle(cornerRadius: BubbleShape.cornerRadius) .fill(style.fill) ) .foregroundStyle(style.content) @@ -147,7 +142,7 @@ private func previewOption( totalVoterCount: 0, hasVoted: false, explanation: nil, options: [previewOption("Red", position: 0), previewOption("Green", position: 1), previewOption("Blue", position: 2)] ), - time: "12:34", isOutgoing: false, replyHeader: nil, onVote: {} + isOutgoing: false, replyHeader: nil, onVote: {} ).bubblePreview() } @@ -160,7 +155,7 @@ private func previewOption( options: [previewOption("Red", position: 0, pct: 75, chosen: true), previewOption("Green", position: 1, pct: 25)] ), - time: "12:35", isOutgoing: true, replyHeader: nil, onVote: {} + isOutgoing: true, replyHeader: nil, onVote: {} ).bubblePreview() } @@ -174,7 +169,7 @@ private func previewOption( previewOption("Paris", position: 1, pct: 60, correct: true), previewOption("Rome", position: 2, pct: 10, correct: false)] ), - time: "12:36", isOutgoing: false, replyHeader: nil, onVote: {} + isOutgoing: false, replyHeader: nil, onVote: {} ).bubblePreview() } @@ -186,7 +181,7 @@ private func previewOption( totalVoterCount: 12, hasVoted: false, explanation: nil, options: [previewOption("Sat", position: 0, pct: 58), previewOption("Sun", position: 1, pct: 42)] ), - time: "09:00", isOutgoing: false, replyHeader: nil, onVote: {} + isOutgoing: false, replyHeader: nil, onVote: {} ).bubblePreview() } #endif diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/ReviewVoiceBubble.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/ReviewVoiceBubble.swift index 5707ad0119..5aa818ff17 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/ReviewVoiceBubble.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/ReviewVoiceBubble.swift @@ -30,7 +30,7 @@ struct ReviewVoiceBubble: View { } .padding(.horizontal, 8).padding(.vertical, 6) .frame(maxWidth: 200, alignment: .leading) - .background(RoundedRectangle(cornerRadius: 10).fill(Color.accentColor)) + .background(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius).fill(Color.accentColor)) .foregroundStyle(.white) .contentShape(Rectangle()) .onTapGesture { controller.toggle(note: note) } diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/VideoBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/VideoBubbleView.swift index 7e7503ac0e..629361ceae 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/VideoBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/VideoBubbleView.swift @@ -3,7 +3,7 @@ import UIKit /// Renders one video bubble: aspect-fit preview thumbnail (downloaded preview, blurred /// minithumbnail, or gray placeholder), centered play overlay, top-trailing duration -/// pill, optional caption below the image, time stamp footer. +/// pill, optional caption below the image. /// /// Drives viewport-based download of the *preview* file id (not the full video) via /// `.onScrollVisibilityChange` on the store — `.onAppear` would fire for every @@ -15,7 +15,6 @@ import UIKit struct VideoBubbleView: View { let video: VideoVisual let caption: String - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? let onTap: () -> Void @@ -46,8 +45,8 @@ struct VideoBubbleView: View { } private var bareVideo: some View { - videoContent(clipImage: true, showTimePill: true) - .contentShape(RoundedRectangle(cornerRadius: 12)) + videoContent(clipImage: true) + .contentShape(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius)) .onTapGesture { onTap() } } @@ -58,7 +57,7 @@ struct VideoBubbleView: View { .padding(.horizontal, 8) .padding(.top, 6) } - videoContent(clipImage: false, showTimePill: false) + videoContent(clipImage: false) .contentShape(Rectangle()) .onTapGesture { onTap() } if !caption.isEmpty { @@ -68,22 +67,17 @@ struct VideoBubbleView: View { .fixedSize(horizontal: false, vertical: true) .padding(.horizontal, 8) } - Text(time) - .font(.system(size: 8)) - .foregroundStyle(style.secondary) - .padding(.horizontal, 8) - .padding(.bottom, 4) } .frame(width: displaySize.width, alignment: .leading) .background(style.fill) - .clipShape(RoundedRectangle(cornerRadius: 12)) + .clipShape(RoundedRectangle(cornerRadius: BubbleShape.cornerRadius)) } - private func videoContent(clipImage: Bool, showTimePill: Bool) -> some View { + private func videoContent(clipImage: Bool) -> some View { ZStack { imageView .frame(width: displaySize.width, height: displaySize.height) - .clipShape(RoundedRectangle(cornerRadius: clipImage ? 12 : 0)) + .clipShape(RoundedRectangle(cornerRadius: clipImage ? BubbleShape.cornerRadius : 0)) .overlay(alignment: .topTrailing) { Text(formatDuration(video.duration)) .font(.system(size: 9)) @@ -93,23 +87,10 @@ struct VideoBubbleView: View { .background(Capsule().fill(.black.opacity(0.5))) .padding(4) } - .overlay(alignment: .bottomTrailing) { - if showTimePill { timePill } - } playOverlay } } - private var timePill: some View { - Text(time) - .font(.system(size: 8)) - .foregroundStyle(.white) - .padding(.horizontal, 4) - .padding(.vertical, 1) - .background(Capsule().fill(.black.opacity(0.5))) - .padding(4) - } - @ViewBuilder private var imageView: some View { if let path = video.preview.previewLocalPath, let img = UIImage(contentsOfFile: path) { @@ -186,7 +167,6 @@ private func videoPreviewStore() -> ChatHistoryStore { videoLocalPath: nil ), caption: "check this clip", - time: "12:34", isOutgoing: false, replyHeader: ReplyHeader( senderName: "Bob", @@ -209,7 +189,6 @@ private func videoPreviewStore() -> ChatHistoryStore { videoLocalPath: nil ), caption: "", - time: "12:34", isOutgoing: false, replyHeader: nil, onTap: {} diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/VideoNoteBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/VideoNoteBubbleView.swift index f42a24ce15..0e1b41ffad 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/VideoNoteBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/VideoNoteBubbleView.swift @@ -4,8 +4,7 @@ import UIKit /// Renders one round-video-note bubble: a chrome-less 150 pt circle with the /// preview image (downloaded thumb → blurred minithumbnail → gray placeholder) /// behind a centered play overlay and a bottom-inside duration pill. Optional -/// reply mini-card (`StickerBubbleView` precedent) sits above. Timestamp footer -/// renders below as plain text on the system background. +/// reply mini-card (`StickerBubbleView` precedent) sits above. /// /// Drives viewport-based download of the thumbnail file id (not the playable /// video file) via `.onScrollVisibilityChange`. The playable file is downloaded @@ -14,7 +13,6 @@ import UIKit /// Tap is unconditional — the viewer handles the not-yet-downloaded state. struct VideoNoteBubbleView: View { let note: VideoNoteVisual - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? let onTap: () -> Void @@ -61,10 +59,6 @@ struct VideoNoteBubbleView: View { store.cancelFileDownload(fileId: id) } } - Text(time) - .font(.system(size: 8)) - .foregroundStyle(.secondary) - .padding(.horizontal, 4) } } @@ -126,7 +120,6 @@ private func videoNotePreviewStore() -> ChatHistoryStore { thumbFileId: nil, minithumbnail: nil, thumbLocalPath: nil, videoLocalPath: nil ), - time: "12:34", isOutgoing: false, replyHeader: nil, onTap: {} @@ -142,7 +135,6 @@ private func videoNotePreviewStore() -> ChatHistoryStore { thumbFileId: nil, minithumbnail: nil, thumbLocalPath: nil, videoLocalPath: nil ), - time: "12:36", isOutgoing: true, replyHeader: nil, onTap: {} @@ -158,7 +150,6 @@ private func videoNotePreviewStore() -> ChatHistoryStore { thumbFileId: nil, minithumbnail: nil, thumbLocalPath: nil, videoLocalPath: nil ), - time: "12:34", isOutgoing: false, replyHeader: ReplyHeader( senderName: "Bob", diff --git a/Telegram/WatchApp/tgwatch Watch App/Messages/VoiceNoteBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Messages/VoiceNoteBubbleView.swift index 67585cb7a0..881516901a 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Messages/VoiceNoteBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Messages/VoiceNoteBubbleView.swift @@ -4,7 +4,6 @@ import SwiftUI /// outgoing) containing a play/pause glyph + 32-bar waveform + duration /// label. Optional reply header sits inside the chrome above the row. /// Caption (when non-empty) renders below the waveform inside the chrome. -/// Time footer renders below the chrome. /// /// Drives priority-1 viewport download via `.onScrollVisibilityChange`. /// Tap goes through `ChatHistoryStore.togglePlayback(_:)`; the store kicks @@ -13,10 +12,8 @@ import SwiftUI struct VoiceNoteBubbleView: View { let note: VoiceNoteVisual let caption: String - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? - let sendingState: SendingState @Environment(ChatHistoryStore.self) private var store @Environment(\.bubbleMetrics) private var metrics @@ -58,13 +55,12 @@ struct VoiceNoteBubbleView: View { .font(.caption) .fixedSize(horizontal: false, vertical: true) } - timeRow } .padding(.horizontal, 8) .padding(.vertical, 6) - .frame(maxWidth: metrics.bubbleMaxWidth, alignment: .leading) + .frame(minWidth: BubbleShape.minSize, maxWidth: metrics.bubbleMaxWidth, minHeight: BubbleShape.minSize, alignment: .leading) .background( - RoundedRectangle(cornerRadius: 10) + RoundedRectangle(cornerRadius: BubbleShape.cornerRadius) .fill(style.fill) ) .foregroundStyle(style.content) @@ -113,28 +109,6 @@ struct VoiceNoteBubbleView: View { .fixedSize(horizontal: true, vertical: false) } - // Time + send-state inline at the bottom-right inside the chrome, matching the - // text-bubble convention. - private var timeRow: some View { - HStack(spacing: 2) { - Spacer(minLength: 0) - Text(time) - .font(.system(size: 8)) - .foregroundStyle(style.secondary) - if isOutgoing { sendStateGlyph } - } - } - - @ViewBuilder - private var sendStateGlyph: some View { - switch sendingState { - case .sent: EmptyView() - case .pending: Image(systemName: "clock").font(.system(size: 8)) - .foregroundStyle(Color.white.opacity(0.7)) - case .failed: Image(systemName: "exclamationmark.circle.fill").font(.system(size: 8)) - .foregroundStyle(.red) - } - } } #if DEBUG @@ -178,8 +152,8 @@ private func previewVoice(id: Int = 1) -> VoiceNoteVisual { #Preview("Voice — incoming, idle") { VoiceNoteBubbleView( note: previewVoice(), caption: "", - time: "12:34", isOutgoing: false, - replyHeader: nil, sendingState: .sent + isOutgoing: false, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -188,8 +162,8 @@ private func previewVoice(id: Int = 1) -> VoiceNoteVisual { #Preview("Voice — outgoing, idle") { VoiceNoteBubbleView( note: previewVoice(id: 2), caption: "", - time: "12:35", isOutgoing: true, - replyHeader: nil, sendingState: .sent + isOutgoing: true, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -198,13 +172,12 @@ private func previewVoice(id: Int = 1) -> VoiceNoteVisual { #Preview("Voice — incoming with reply") { VoiceNoteBubbleView( note: previewVoice(id: 3), caption: "", - time: "12:36", isOutgoing: false, + isOutgoing: false, replyHeader: ReplyHeader( senderName: "Bob", snippet: "anchor message earlier in the chat", minithumbnail: nil, isOutgoing: false - ), - sendingState: .sent + ) ) .bubblePreview() .environment(previewStore()) @@ -214,8 +187,8 @@ private func previewVoice(id: Int = 1) -> VoiceNoteVisual { VoiceNoteBubbleView( note: previewVoice(id: 4), caption: "Listen to this part carefully", - time: "12:37", isOutgoing: false, - replyHeader: nil, sendingState: .sent + isOutgoing: false, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -227,8 +200,8 @@ private func previewVoice(id: Int = 1) -> VoiceNoteVisual { voiceFileId: 5, duration: 5, mimeType: "audio/ogg", waveform: Data(), caption: "", localPath: nil ), - caption: "", time: "12:38", isOutgoing: false, - replyHeader: nil, sendingState: .sent + caption: "", isOutgoing: false, + replyHeader: nil ) .bubblePreview() .environment(previewStore()) diff --git a/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerBubbleView.swift b/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerBubbleView.swift index eef28ef49d..d07c38fb5b 100644 --- a/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerBubbleView.swift +++ b/Telegram/WatchApp/tgwatch Watch App/Stickers/StickerBubbleView.swift @@ -2,13 +2,12 @@ import SwiftUI import TDLibKit /// One sticker bubble. No rounded-rect chat-bubble chrome — sticker renders directly -/// against the chat background, with sender name above (incoming groups only) and time -/// stamp below. Drives the same visibility-based download flow as PhotoBubbleView. +/// against the chat background, with sender name above (incoming groups only). +/// Drives the same visibility-based download flow as PhotoBubbleView. struct StickerBubbleView: View { let sticker: StickerVisual let senderName: String? var senderColorIndex: Int? = nil - let time: String let isOutgoing: Bool let replyHeader: ReplyHeader? @@ -47,10 +46,6 @@ struct StickerBubbleView: View { .onScrollVisibilityChange(threshold: 0.01) { visible in handleVisibility(visible) } - Text(time) - .font(.system(size: 8)) - .foregroundStyle(.secondary) - .padding(.horizontal, 4) } } @@ -76,8 +71,9 @@ struct StickerBubbleView: View { .font(.caption) .padding(.horizontal, 8) .padding(.vertical, 4) + .frame(minWidth: BubbleShape.minSize, minHeight: BubbleShape.minSize) .background( - RoundedRectangle(cornerRadius: 10) + RoundedRectangle(cornerRadius: BubbleShape.cornerRadius) .fill(style.fill) ) .foregroundStyle(style.content) @@ -182,7 +178,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb #Preview("WEBP — downloaded") { StickerBubbleView( sticker: previewSticker(format: .webp, withFile: true), - senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil + senderName: nil, isOutgoing: false, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -191,7 +187,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb #Preview("WEBP — downloading, thumbnail available") { StickerBubbleView( sticker: previewSticker(format: .webp, withFile: false, withThumb: true), - senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil + senderName: nil, isOutgoing: false, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -200,7 +196,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb #Preview("WEBP — downloading, no thumbnail") { StickerBubbleView( sticker: previewSticker(format: .webp, withFile: false), - senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil + senderName: nil, isOutgoing: false, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -209,7 +205,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb #Preview("TGS — downloaded") { StickerBubbleView( sticker: previewSticker(format: .tgs, withFile: true), - senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil + senderName: nil, isOutgoing: false, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -218,7 +214,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb #Preview("TGS — downloading, thumbnail available") { StickerBubbleView( sticker: previewSticker(format: .tgs, withFile: false, withThumb: true), - senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil + senderName: nil, isOutgoing: false, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -227,7 +223,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb #Preview("WEBM — unsupported fallback") { StickerBubbleView( sticker: previewSticker(format: .unsupported, withFile: false), - senderName: nil, time: "12:34", isOutgoing: false, replyHeader: nil + senderName: nil, isOutgoing: false, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -236,7 +232,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb #Preview("Group sticker — sender name above") { StickerBubbleView( sticker: previewSticker(format: .webp, withFile: true), - senderName: "Alice", time: "12:34", isOutgoing: false, replyHeader: nil + senderName: "Alice", isOutgoing: false, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -246,7 +242,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb StickerBubbleView( sticker: previewSticker(format: .webp, withFile: true), senderName: "Alice", senderColorIndex: paletteIndex(for: 200), - time: "12:34", isOutgoing: false, replyHeader: nil + isOutgoing: false, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -255,7 +251,7 @@ private func previewSticker(format: StickerFormatKind, withFile: Bool, withThumb #Preview("Outgoing sticker") { StickerBubbleView( sticker: previewSticker(format: .webp, withFile: true), - senderName: nil, time: "12:34", isOutgoing: true, replyHeader: nil + senderName: nil, isOutgoing: true, replyHeader: nil ) .bubblePreview() .environment(previewStore()) @@ -275,7 +271,7 @@ private func previewReplyHeader(toText: String = "anchor", isOutgoing: Bool = fa #Preview("Sticker WEBP — with reply card above") { StickerBubbleView( sticker: previewSticker(format: .webp, withFile: true), - senderName: nil, time: "12:34", isOutgoing: false, + senderName: nil, isOutgoing: false, replyHeader: previewReplyHeader(toText: "anchor message") ) .bubblePreview() @@ -285,7 +281,7 @@ private func previewReplyHeader(toText: String = "anchor", isOutgoing: Bool = fa #Preview("Sticker TGS — with reply card above") { StickerBubbleView( sticker: previewSticker(format: .tgs, withFile: true), - senderName: nil, time: "12:34", isOutgoing: false, + senderName: nil, isOutgoing: false, replyHeader: previewReplyHeader(toText: "look at this clip") ) .bubblePreview() @@ -295,7 +291,7 @@ private func previewReplyHeader(toText: String = "anchor", isOutgoing: Bool = fa #Preview("Sticker outgoing — with reply card above") { StickerBubbleView( sticker: previewSticker(format: .webp, withFile: true), - senderName: nil, time: "12:34", isOutgoing: true, + senderName: nil, isOutgoing: true, replyHeader: previewReplyHeader(toText: "thanks!", isOutgoing: true) ) .bubblePreview() diff --git a/Telegram/WatchApp/tgwatch.xcodeproj/project.pbxproj b/Telegram/WatchApp/tgwatch.xcodeproj/project.pbxproj index 9b0fe1557e..6514e9d26f 100644 --- a/Telegram/WatchApp/tgwatch.xcodeproj/project.pbxproj +++ b/Telegram/WatchApp/tgwatch.xcodeproj/project.pbxproj @@ -92,6 +92,7 @@ C62226963E54795B2808A52C /* MapSnapshotRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFF41E60C5477C20897CD00 /* MapSnapshotRenderer.swift */; }; C6A64A26FC7DC74BDEC91308 /* ChatHistoryLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2C84B842AD08D079BFF08A0 /* ChatHistoryLoader.swift */; }; C94BD040D985FC0090CDBF84 /* StickerPickerLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC230FDDDFAA063019744A0E /* StickerPickerLoader.swift */; }; + CBA3FEDEEA413559F700B6E3 /* BubbleShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD18F48DC98A3857B32BBC6F /* BubbleShape.swift */; }; D1082945B5785B08F820FB78 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93535C2021AB2E7B17F593AA /* LoadingView.swift */; }; D1E96E63A3AE93646AD3F532 /* AudioBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C3B71804A398A0100375E86 /* AudioBubbleView.swift */; }; D5515C5A9286E0372DBFB7F9 /* QRCodeGenerator in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A04697FEA6C4614973F19 /* QRCodeGenerator */; }; @@ -206,6 +207,7 @@ D4AF5A180BB3A37832AC5349 /* VideoNoteBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoNoteBubbleView.swift; sourceTree = ""; }; D50662DC1777F2BEE53A0129 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D9CD96277A18D97BD335BBC0 /* StickerSetDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerSetDetailView.swift; sourceTree = ""; }; + DD18F48DC98A3857B32BBC6F /* BubbleShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleShape.swift; sourceTree = ""; }; DDCE599C8D50EE8832B4A797 /* ReplyFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyFormatters.swift; sourceTree = ""; }; DF0F4F15CB43635717B31B92 /* PollVoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollVoteView.swift; sourceTree = ""; }; DF393D904BF677AC5721EC7E /* ServiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceMessageView.swift; sourceTree = ""; }; @@ -335,6 +337,7 @@ CBAFC996C8EF743C7C598389 /* AudioVisual.swift */, 0F9127473C7DAD18736234B3 /* AVRecordingBackend.swift */, 674916153E803CA73D43BE6C /* BubbleMetrics.swift */, + DD18F48DC98A3857B32BBC6F /* BubbleShape.swift */, 3EEAE6237003C5CDA788E928 /* BubbleStyle.swift */, C2C84B842AD08D079BFF08A0 /* ChatHistoryLoader.swift */, 182DA99AD90B93DE2FB37A41 /* ChatHistoryStore.swift */, @@ -520,6 +523,7 @@ 5E9A6B94F1B917CB849B3EBC /* AvatarView.swift in Sources */, 03074928DC54019BE6106ECD /* AvatarVisual.swift in Sources */, 4236CA9B5AA90ACEB17814DC /* BubbleMetrics.swift in Sources */, + CBA3FEDEEA413559F700B6E3 /* BubbleShape.swift in Sources */, 868426A2768FE7349CD4AAAD /* BubbleStyle.swift in Sources */, 4505C5628AF9258938D623D7 /* CachedChat.swift in Sources */, C6A64A26FC7DC74BDEC91308 /* ChatHistoryLoader.swift in Sources */,