mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-11 05:45:48 +00:00
* ios: stop showing tails for non revealed moderated or blocked items * simplify --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
176 lines
6.2 KiB
Swift
176 lines
6.2 KiB
Swift
//
|
|
// ChatItemClipShape.swift
|
|
// SimpleX (iOS)
|
|
//
|
|
// Created by Levitating Pineapple on 04/07/2024.
|
|
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
|
//
|
|
|
|
import SwiftUI
|
|
import SimpleXChat
|
|
|
|
/// Modifier, which provides clipping mask for ``ChatItemWithMenu`` view
|
|
/// and it's previews: (drag interaction, context menu, etc.)
|
|
/// Supports [Dynamic Type](https://developer.apple.com/documentation/uikit/uifont/scaling_fonts_automatically)
|
|
/// by retaining pill shape, even when ``ChatItem``'s height is less that twice its corner radius
|
|
struct ChatItemClipped: ViewModifier {
|
|
@AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var roundness = defaultChatItemRoundness
|
|
@AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var tailEnabled = true
|
|
private let chatItem: (content: CIContent, chatDir: CIDirection)?
|
|
private let tailVisible: Bool
|
|
|
|
init() {
|
|
self.chatItem = nil
|
|
self.tailVisible = false
|
|
}
|
|
|
|
init(_ ci: ChatItem, tailVisible: Bool) {
|
|
self.chatItem = (ci.content, ci.chatDir)
|
|
self.tailVisible = tailVisible
|
|
}
|
|
|
|
private func shapeStyle() -> ChatItemShape.Style {
|
|
if let ci = chatItem {
|
|
switch ci.content {
|
|
case
|
|
.sndMsgContent,
|
|
.rcvMsgContent,
|
|
.rcvDecryptionError,
|
|
.rcvIntegrityError,
|
|
.invalidJSON:
|
|
let tail = if let mc = ci.content.msgContent, mc.isImageOrVideo && mc.text.isEmpty {
|
|
false
|
|
} else {
|
|
tailVisible
|
|
}
|
|
return tailEnabled
|
|
? .bubble(
|
|
padding: ci.chatDir.sent ? .trailing : .leading,
|
|
tailVisible: tail
|
|
)
|
|
: .roundRect(radius: msgRectMaxRadius)
|
|
case .rcvGroupInvitation, .sndGroupInvitation:
|
|
return .roundRect(radius: msgRectMaxRadius)
|
|
default: return .roundRect(radius: 8)
|
|
}
|
|
} else {
|
|
return .roundRect(radius: msgRectMaxRadius)
|
|
}
|
|
}
|
|
|
|
func body(content: Content) -> some View {
|
|
let clipShape = ChatItemShape(
|
|
roundness: roundness,
|
|
style: shapeStyle()
|
|
)
|
|
content
|
|
.contentShape(.dragPreview, clipShape)
|
|
.contentShape(.contextMenuPreview, clipShape)
|
|
.clipShape(clipShape)
|
|
}
|
|
}
|
|
|
|
struct ChatTailPadding: ViewModifier {
|
|
func body(content: Content) -> some View {
|
|
content.padding(.horizontal, -msgTailWidth)
|
|
}
|
|
}
|
|
|
|
private let msgRectMaxRadius: Double = 18
|
|
private let msgBubbleMaxRadius: Double = msgRectMaxRadius * 1.2
|
|
private let msgTailWidth: Double = 9
|
|
private let msgTailMinHeight: Double = msgTailWidth * 1.254 // ~56deg
|
|
private let msgTailMaxHeight: Double = msgTailWidth * 1.732 // 60deg
|
|
|
|
struct ChatItemShape: Shape {
|
|
fileprivate enum Style {
|
|
case bubble(padding: HorizontalEdge, tailVisible: Bool)
|
|
case roundRect(radius: Double)
|
|
}
|
|
|
|
fileprivate let roundness: Double
|
|
fileprivate let style: Style
|
|
|
|
func path(in rect: CGRect) -> Path {
|
|
switch style {
|
|
case let .bubble(padding, tailVisible):
|
|
let w = rect.width
|
|
let h = rect.height
|
|
let rxMax = min(msgBubbleMaxRadius, w / 2)
|
|
let ryMax = min(msgBubbleMaxRadius, h / 2)
|
|
let rx = roundness * rxMax
|
|
let ry = roundness * ryMax
|
|
let tailHeight = min(msgTailMinHeight + roundness * (msgTailMaxHeight - msgTailMinHeight), h / 2)
|
|
var path = Path()
|
|
// top side
|
|
path.move(to: CGPoint(x: rx, y: 0))
|
|
path.addLine(to: CGPoint(x: w - rx, y: 0))
|
|
if roundness > 0 {
|
|
// top-right corner
|
|
path.addQuadCurve(to: CGPoint(x: w, y: ry), control: CGPoint(x: w, y: 0))
|
|
}
|
|
if rect.height > 2 * ry {
|
|
// right side
|
|
path.addLine(to: CGPoint(x: w, y: h - ry))
|
|
}
|
|
if roundness > 0 {
|
|
// bottom-right corner
|
|
path.addQuadCurve(to: CGPoint(x: w - rx, y: h), control: CGPoint(x: w, y: h))
|
|
}
|
|
// bottom side
|
|
if tailVisible {
|
|
path.addLine(to: CGPoint(x: -msgTailWidth, y: h))
|
|
if roundness > 0 {
|
|
// bottom-left tail
|
|
// distance of control point from touch point, calculated via ratios
|
|
let d = tailHeight - msgTailWidth * msgTailWidth / tailHeight
|
|
// tail control point
|
|
let tc = CGPoint(x: 0, y: h - tailHeight + d * sqrt(roundness))
|
|
// bottom-left tail curve
|
|
path.addQuadCurve(to: CGPoint(x: 0, y: h - tailHeight), control: tc)
|
|
} else {
|
|
path.addLine(to: CGPoint(x: 0, y: h - tailHeight))
|
|
}
|
|
if rect.height > ry + tailHeight {
|
|
// left side
|
|
path.addLine(to: CGPoint(x: 0, y: ry))
|
|
}
|
|
} else {
|
|
path.addLine(to: CGPoint(x: rx, y: h))
|
|
path.addQuadCurve(to: CGPoint(x: 0, y: h - ry), control: CGPoint(x: 0 , y: h))
|
|
if rect.height > 2 * ry {
|
|
// left side
|
|
path.addLine(to: CGPoint(x: 0, y: ry))
|
|
}
|
|
}
|
|
if roundness > 0 {
|
|
// top-left corner
|
|
path.addQuadCurve(to: CGPoint(x: rx, y: 0), control: CGPoint(x: 0, y: 0))
|
|
}
|
|
path.closeSubpath()
|
|
return switch padding {
|
|
case .leading: path
|
|
case .trailing: path
|
|
.scale(x: -1, y: 1, anchor: .center)
|
|
.path(in: rect)
|
|
}
|
|
case let .roundRect(radius):
|
|
return Path(roundedRect: rect, cornerRadius: radius * roundness)
|
|
}
|
|
}
|
|
|
|
var offset: Double? {
|
|
switch style {
|
|
case let .bubble(padding, isTailVisible):
|
|
if isTailVisible {
|
|
switch padding {
|
|
case .leading: -msgTailWidth
|
|
case .trailing: msgTailWidth
|
|
}
|
|
} else { 0 }
|
|
case .roundRect: 0
|
|
}
|
|
}
|
|
|
|
}
|