import Foundation
import UIKit
/// Configuration constants for chat view
enum ChatViewConfiguration {
/// If `strictMode` is enabled failing sanity checks do not only print errors but may also crash for release builds
static let strictMode = false
// TODO: Scale the value according with dynamic type? (IOS-3998)
/// Inset at top of all chats. This also adds to the top inset the sticky section header.
static let topInset = ChatBubble.defaultTopBottomInset
/// Default (additional) inset at the bottom of the chat (scroll) view
static let bottomInset =
if #available(iOS 26.0, *) {
ChatBubble.defaultTopBottomInset + (ChatBubble.defaultLeadingTrailingInset / 2) + 4
}
else {
ChatBubble.defaultTopBottomInset + (ChatBubble.defaultLeadingTrailingInset / 2)
}
/// Default (additional) inset at the bottom of the chat (scroll) view of group chats
static let groupBottomInset =
if #available(iOS 26.0, *) {
ChatBubble.defaultGroupTopBottomInset + (ChatBubble.defaultLeadingTrailingInset / 2) + 4
}
else {
ChatBubble.defaultGroupTopBottomInset + (ChatBubble.defaultLeadingTrailingInset / 2)
}
/// Deciding whether a voice draft must be saved after the chat disappeared (see
/// `ChatViewController.saveDraftIfChatWasLeft()`)
enum VoiceDraft {
/// Delay before checking whether the chat is back on screen. Leaves time to the navigation router to put it
/// into its new navigation controller after a split view collapse or expand.
static let checkInterval: TimeInterval = 0.3
/// Longest wait for a running view transition to end
static let transitionTimeout: TimeInterval = 3
}
/// Profile on the top in the navigation bar
enum Profile {
// We use fixed font sizes for the profile in the navigation bar as the navigation bar doesn't adapt
// its height for different content size categories and also clips them at XXL (no accessibility content
// sizes are reported)
/// Constant font for contact or group name
static let nameFont = UIFont.systemFont(ofSize: 16, weight: .semibold)
/// Constant font for members lists
static let membersListFont = UIFont.systemFont(ofSize: 12)
/// Combined leading and trailing offset from the navigation width (only applies with non-regular horizontal
/// size class)
static let combinedLeadingAndTrailingOffset: CGFloat =
if #available(iOS 26, *) {
// Based on iPhone 17 and iOS 26.3
2 * 72
}
else {
2 * 62
}
/// Combined leading and trailing offset from the navigation width when the back button has an unread message
/// count in it (only applies with non-regular horizontal size class)
static let combinedLeadingAndTrailingOffsetWithUnreadMessages: CGFloat =
if #available(iOS 26, *) {
// Works up to iPhone 17 Pro. See `ChatProfileView` for an adjustment for bigger screens
2 * 90
}
else {
combinedLeadingAndTrailingOffset
}
/// Combined leading and trailing offset from the navigation width when the horizontal size class is regular
/// (e.g. iPad)
static let combinedLeadingAndTrailingOffsetRegularSizeClass: CGFloat =
if #available(iOS 26, *) {
// Based on iPad Air 11" (M3) and iOS 26.1
2 * 90
}
else {
2 * 62
}
/// Top and bottom content insets
///
/// All content is offset by these insets on top and bottom
static let topAndBottomInset: CGFloat = 4
/// Space between profile picture and rest of profile information
static let profilePictureAndInfoSpace: CGFloat = 8
/// Height of the profile picture
static let profilePictureHeight: CGFloat = 44
/// Height of verification level below name
static let verificationLevelHeight: CGFloat = 7
/// Vertical space between contact/group name and member list below
static let nameAndMembersListRegularSpacing: CGFloat = 1
/// Horizontal space between contact/group name and trailing member list
static let nameAndMembersListCompactSpacing: CGFloat = 8
/// Call bar button item symbol name
static let callSymbolName = "threema.phone"
/// Group call bar button item symbol name
static let groupCallSymbolName = "threema.phone"
/// Custom ballot bar button item shown in group chats
enum BallotButton {
/// Ballot symbol name
static let ballotSymbolName = "chart.pie"
/// Ballot symbol configuration to approximate the appearance of a normal bar button item
static let symbolConfiguration = UIImage.SymbolConfiguration(scale: .large)
/// Offset of badge from ballot symbol ratio
static let badgeOffsetFromBallotSymbolRatio: CGFloat = 2 / 5
/// Min size of button touch area
static let minimumTouchTargetWidth: CGFloat = 44
/// Correction of offset to approximate a normal bar button item position
static let offsetCorrection: CGFloat = 8 // This should always be <= 8
}
}
enum SectionHeader {
/// Default top and bottom insets for section headers (we remove the inset coming from the bubbles around it)
static let defaultTopAndBottomInset = 16.0 - ChatBubble.defaultTopBottomInset
/// Corner radius for background
static let cornerRadius = 12.5
/// Blur effect style for background and vibrancy effect
static let blurEffectStyle = UIBlurEffect.Style.systemThinMaterial
/// Configuration for date label in section headers
enum DateLabel {
static let font = UIFont.preferredFont(forTextStyle: .footnote).bold()
/// Default top and bottom inset of the label
static let defaultTopBottomInset = 3.5
/// Default leading and trailing inset of the label
static let defaultLeadingTrailingInset = 10.0
}
}
/// Cache cell heights
static let enableCellHeightCaching = false
/// Estimate and cache (estimated) cell heights
static let enableEstimatedCellHeightCaching = false
/// Animation duration
static let contextMenuBackgroundShowHideAnimationDuration = 0.25
/// Configuration for background of message cells
enum ChatBubble {
/// Default corner radius for all bubbles with default and larger `preferredContentSizeCategory`s (i.e. `>=
/// .large`)
static let cornerRadius = 16.5
/// Default corner radius for all bubbles with smaller than default `preferredContentSizeCategory`s (i.e. `<
/// .large`)
static let smallerContentSizeConfigurationCornerRadius = 15.5
/// Default top and bottom space between chat bubbles (4 means 8 in total)
static let defaultTopBottomInset = 4.0
/// Default top and bottom space between chat bubbles in group chats (8 means 16 in total)
static let defaultGroupTopBottomInset = 8.0
/// Default top and bottom space between chat bubbles when they are grouped (2 means 4 in total)
static let groupedTopBottomInset = 2.0
/// Bottom inset of the reactions stack view to the chat bubble
static let reactionBottomInset = 5.0
/// Default leading and trailing space of chat bubbles
static let defaultLeadingTrailingInset: CGFloat = 8
/// Ratio of readable content width that should be the maximum width of a bubble
static let defaultMaxWidthRatio: CGFloat = 0.8
/// Ratio of readable content width that should be the maximum width of a bubble of a voice message cell
static let voiceMessageCellMaxWidthRatio: CGFloat = 0.85
/// Duration in seconds of the size change animation if subviews are added to or removed from the chatBubbleView
/// Should in general be less than or equal to the row height change animation of the table view because
/// otherwise you will get overlapping cells which doesn't look nice.
static let bubbleSizeChangeAnimationDurationInSeconds: CGFloat = 0.25
/// Duration in seconds of the show/hide animation of the date and state view for every message
/// Should in general be less than or equal to the row height change animation of the table view because
/// otherwise you will get overlapping view which doesn't look nice.
static let dateAndStateViewShowAndHideAnimationDurationInSeconds: CGFloat = 0.25
enum SwipeInteraction {
/// This blocks the swipe to reply interaction within n pixels of the left border
static let swipeDeadZone: CGFloat = 50
/// Duration for the scaling animation when the swipe to reply action was activated
static let startReplyIconAnimationDuration = 0.075
/// Scale factor for the scaling animation when the swipe to reply action as activated.
/// Should be chosen such that the multiplicative inverse is a rational number representable with the same
/// type.
static let startReplyAnimationScaleFactor = 1.25
/// Factor by which the cell moves slower than the finger on the screen after the swipe to reply action was
/// activated
static let bubbleSlowdownFactorReply: Double = 1 / 8
/// Factor by which the cell moves slower than the finger on the screen after the swipe to details action
/// was activated
static let bubbleSlowdownFactorDetails: Double = 1 / 4
/// Number of pixels to swipe before the action activates
static let swipeActionOffsetThreshold: Double = 55
/// Number of pixels by which the reply icon is inset from the left edge of the cell bubble
static let iconInset: Double = swipeActionOffsetThreshold / 1.75
/// The SFSymbol name of the icon to be used when swiping
static let replySymbolName = "arrowshape.turn.up.left.fill"
/// Duration for the reset animation when cell swiping is cancelled
static let resetDuration = 0.5
/// Spring dampening for the reset animation when cell swiping is cancelled
static let springDampening = 0.8
}
enum MessageDetailsInteraction {
/// Percentage of the screen that has to be swiped across for the DetailsView to commit to appearing
static let detailsCommitPercentage = 0.3
/// Duration in seconds how long the transition to the DetailsView should take
static let transitionDuration = 0.3
}
enum HighlightedAnimation {
/// Duration for which the selected state of the cell is faded in and out
/// The total duration for the animation is 2 x blinkFadeInOut + highlightedDuration
static let highlightFadeInOutDuration = 0.5
/// Duration for which the cell should stay highlighted when used to show it for the user
static let highlightedDurationLong = 1.75
/// Duration for which the cell should stay highlighted when tapped
static let highlightedDurationTap = 0.15
/// Delay after the tableView has stopped scrolling and the blinkFadeIn should start
static let highlightDelayAfterScroll = 0.125
}
enum RetryAndCancelButton {
/// Name of the SFSymbol displayed in button
static let symbolName = "arrow.clockwise"
/// Spacing between the button and the bubble
static let buttonChatBubbleSpacing = 16.0
}
}
enum GroupCells {
/// Default top and bottom space between name label and cell content
static let nameLabelDefaultTopBottomInset: CGFloat = 4
/// Default font for name labels in group message cells
static let nameLabelFont = UIFont.preferredFont(forTextStyle: .footnote).bold()
/// Inset from the leading side of the cell
static let profilePictureLeadingInset: CGFloat = ChatBubble.defaultLeadingTrailingInset
/// Horizontal space between profile picture and message cell
static let profilePictureCellSpace: CGFloat = 6.0
/// Width / height of profile picture
static let maxProfilePictureSize: CGFloat = 25
/// Offset of profile picture in relation to chat bubble
static let profilePictureVerticalOffset: CGFloat = maxProfilePictureSize / 4
}
enum CellGrouping {
/// The maximum amount of time to have passed between two messages for them to still be grouped together in
/// seconds
static let maxDurationForGroupingTogether = 0.5 * 60 * 60
/// Enable date and state grouping (if both are identical)
static let enableDateAndStateGrouping = true
}
enum SystemMessage {
/// Default top and bottom space between system message (8 means 16 in total)
static let defaultTopBottomInset = 8.0
/// Default top and bottom space between system message when they are grouped (4 means 8 in total)
static let groupedDefaultTopBottomInset = 4.0
/// Space between icon and label in WorkConsumerInfoSystemMessageCell
static let typeIconLabelSpace = 4.0
/// Configuration for background of system message cells
enum Background {
static let cornerRadius: CGFloat = 12.5
/// Default top and bottom inset of content in the system message bubble
static let defaultSystemMessageTopBottomInset: CGFloat = 4.5
/// Default leading and trailing inset of content in the system message bubble
static let defaultSystemMessageLeadingTrailingInset: CGFloat = 10
}
}
enum TypingIndicator {
enum AttachedBubble {
/// Offset from `frame.minX` where `frame` is the frame of the chat bubble
static let xOffset: CGFloat = 0
/// Offset from `frame.maxY` where `frame` is the frame of the chat bubble
static let yOffset: CGFloat = -10
/// Width of the rectangle in which the oval is contained
static let width: CGFloat = 8
/// Height of the rectangle in which the oval is contained
static let height: CGFloat = 8
}
enum SmallBubble {
/// Offset from `frame.minX` where `frame` is the frame of the chat bubble
static let xOffset: CGFloat = -3
/// Offset from `frame.maxY` where `frame` is the frame of the chat bubble
static let yOffset: CGFloat = -3
/// Width of the rectangle in which the oval is contained
static let width: CGFloat = 4
/// Height of the rectangle in which the oval is contained
static let height: CGFloat = 4
}
enum Animation {
/// Darkest color used in the animation
static let minimumWhiteValue = 0.2
/// Lightest color used in the animation
static let maximumWhiteValue = 0.7
/// Duration of the animation
static let animationDuration = 1.25
/// Total frames for one transition. In total there will be `3 * (totalFrames + 1)` frames used in the whole
/// animation
static let totalFrames: Double = 60
/// Offset into the color progression where `i` of `offseti` indicates the i-th bubble from the left
static let offset1 = 120
static let offset2 = 80
static let offset3 = 40
}
enum View {
/// Inset from leadingAnchor of the cells `contentView`. Set to `abs(SmallBubble.xOffset)` for aligning the
/// left most pixel to the other bubbles and to zero for aligning the left most point of the bubble ignoring
/// arrows and bubbles.
static let leadingInsetConstant: CGFloat = ChatBubble.defaultLeadingTrailingInset
}
}
/// Content inside the bubble
enum Content {
/// Default top and bottom inset of content in chat bubble
static let defaultTopBottomInset: CGFloat = 7
/// Default leading and trailing inset of content in chat bubble
static let defaultLeadingTrailingInset: CGFloat = 12
/// Vertical space between content and metadata of message cell
static let contentAndMetadataSpace: CGFloat = 4
/// Vertical space between text and secondary text label
static let textAndSecondaryTextSpace: CGFloat = 2
/// Horizontal space between icon and text labels of message cell
static let defaultIconAndTextSpace: CGFloat = 8
/// Horizontal space between icon and text labels of message cell
static let defaultIconCenterInset: CGFloat = 8
}
/// Edited message view
enum EditedMessage {
/// Width of the bar in a edit message
static let barWidth: CGFloat = 2
/// Distance between edit message bar and text
static let barTextDistance: CGFloat = 6
/// Distance between name and edit message text
static let nameTextDistance: CGFloat = 2
/// Default font for the title label in edit message
static let nameFont = UIFont.preferredFont(forTextStyle: .caption1).bold()
/// Maximal lines of edit message label
static let maxLines = 7
/// Corner radius of thumbnail
static let thumbnailCornerRadius = ChatBubble.cornerRadius - ChatBubble.defaultLeadingTrailingInset
/// Default size of thumbnail
static let thumbnailDefaultSize = 40.0
}
/// Quote view
enum Quote {
/// Width of the bar in a quote
static let quoteBarWidth: CGFloat = 2
/// Distance between quote view and text cell contents
static let quoteTextCellDistance: CGFloat = Content.defaultTopBottomInset
/// Distance between quote bar and text
static let quoteBarTextDistance: CGFloat = 6
/// Distance between name and quote text
static let quoteNameTextDistance: CGFloat = 2
/// Default font for the name label in quotes
static let nameFont = UIFont.preferredFont(forTextStyle: .caption1).bold()
/// Maximal length of quoted string
static let maxQuoteLength = 200
/// Maximal lines of quote label
static let maxQuoteLines =
if #available(iOS 26.0, *) {
3
}
else {
7
}
/// Corner radius of thumbnail
static let thumbnailCornerRadius = ChatBubble.cornerRadius - ChatBubble.defaultLeadingTrailingInset
/// Inset of trailing inset of thumbnail
static let thumbnailTrailingInset = ChatBubble.defaultLeadingTrailingInset
/// Default size of thumbnail
static let thumbnailDefaultSize = 40.0
}
/// Default text
enum Text {
/// Default font for text messages and captions
static let font = UIFont.preferredFont(forTextStyle: .body)
static let textStyle = UIFont.TextStyle.body
static let emojiFont = UIFont.preferredFont(forTextStyle: emojiTextStyle)
static let emojiTextStyle = UIFont.TextStyle.largeTitle
/// Configuration of symbols
static let symbolConfiguration = UIImage.SymbolConfiguration(
textStyle: .body,
scale: .default
)
}
/// Secondary text
/// E.g. Address at LocationMessage
enum SecondaryText {
/// Default font for secondary text labels
static let font = UIFont.preferredFont(forTextStyle: .footnote)
/// Used to make SFSymbols dynamic
static let fontStyle: UIFont.TextStyle = .footnote
/// Scale of state symbol
static let symbolScale: UIImage.SymbolScale = .default
/// Configuration of Symbol
static let symbolConfiguration = UIImage.SymbolConfiguration(
textStyle: ChatViewConfiguration.SecondaryText.fontStyle,
scale: ChatViewConfiguration.SecondaryText.symbolScale
)
static let smallSymbolConfiguration = UIImage.SymbolConfiguration(
textStyle: ChatViewConfiguration.SecondaryText.fontStyle,
scale: .small
)
}
/// Thumbnail views
enum Thumbnail {
/// Default margin around thumbnail
static let defaultMargin: CGFloat = 2
/// Corner radius of thumbnail
static let cornerRadius = ChatBubble.cornerRadius - defaultMargin
/// Max height of thumbnails (this has higher priority than the aspect ratio)
static let maxHeight = 350.0
/// Default aspect ratio of thumbnail view
static let defaultAspectRatio = 1.0
/// Min aspect ratio of thumbnail view (height/width)
static let minAspectRatio = 0.3
/// Max aspect ratio of thumbnail view (height/width)
static let maxAspectRatio = 1.4
/// The amount of alpha that gets applied on the thumbnail when its cell is highlighted
static let highlightingAlpha: CGFloat = 0.8
/// Space between state and progress
static let defaultStateAndProgressSpace: CGFloat = 12
/// The maximum amount of bytes a blob may have in order to be rendered in full resolution
static let maximumBytesForFullPreview = 1_500_000
}
/// File views
enum File {
/// Font for file name
static let fileNameFont = UIFont.preferredFont(forTextStyle: .subheadline)
/// Offset of state circle from file icon
static let defaultStateCircleOffsetFromFileIcon: CGFloat = 3
/// Default margin around tap view
static let defaultMargin: CGFloat = 2
/// Corner radius of tap view
static let cornerRadius = ChatBubble.cornerRadius - defaultMargin
/// Inset of the content of the tap view to the border of the tap view
static let fileLeadingTrailingInset = Content.defaultLeadingTrailingInset - defaultMargin
/// Inset of the content of the tap view to the border of the tap view
static let fileTopBottomInset = Content.defaultTopBottomInset - defaultMargin
/// Minimal space between file size and date and state
static let minFileSizeAndDateAndStateSpace: CGFloat = 8
/// Vertical Space between name and metadata
static let fileNameAndMetadataSpace: CGFloat = 4
/// Horizontal space between file icon and file info
static let fileIconAndFileInfoSpace: CGFloat = 8
}
/// Voice Message Views
enum VoiceMessage {
/// The point height of the waveform view. the height constraint is set to this.
/// Together with `PlaybackStateButton.circleFillSymbolSize` this is the major influence on the cell height.
static let waveformHeight: CGFloat = 35
/// UIStackView spacing between the speed button / mic icon and the waveform view
static let waveformSpeedIconStackViewSpacing: CGFloat = 2.0
/// TimeInterval in which progress is reported by the voice message cell delegate back to the relevant voice
/// message cell
static let progressCallbackInterval: CGFloat = 0.01
/// Button showing either a mic symbol or the current playback speed when playing a voice message
enum SpeedButton {
/// Duration for the animation shown when switching between mic symbol and playback speed button
static let hideOrShowAnimationDuration: CGFloat = 0.25
/// Animation option for the animation shown when switching between mic symbol and playback speed button
static let hideOrShowAnimationOptions: UIView.AnimationOptions = .curveEaseInOut
/// Inset for the text in the playback speed button
static let topBottomInset: CGFloat = 3
static let leftRightInset: CGFloat = 6
/// Symbol configuration for the mic symbol
static let micIconSymbolConfigurationScale: UIImage.SymbolScale = .large
/// Corner radius of the background around the speed label
static let cornerRadius: CGFloat = 10
/// Label showing the current playback speed
enum SpeedLabel {
/// Font used for the speed label
static let font = UIFont.monospacedDigitSystemFont(ofSize: 14.0, weight: .regular)
}
}
/// Button indicating the current blob state or play state if appropriate
enum PlaybackStateButton {
/// Symbol inset from the border of the button for play state symbols
static let circleFillSymbolInset: CGFloat = 5
/// Symbol size for play and pause symbols used in the playback state button
static let circleFillSymbolSize: CGFloat = 44
}
/// View displaying the waveform for the voice message
enum WaveformView {
/// inset of the UIImageView displaying the waveform from it's parent view
static let waveformImageInset: CGFloat = 0
/// The width of a single bar in the waveform view
static let singleBarWidth: CGFloat = 2
/// The spacing between two bars in the waveform view
static let barSpacing: CGFloat = 2
/// Height of the rendered waveform image. Ideally this is the height of the waveform's parent view minus
/// two times the insets
static let waveformRenderHeight: CGFloat = 35
}
enum NeighborPlayback {
static let maxDurationForNeighborAutomaticPlaybackInSeconds = 0.5 * 60 * 60
}
}
enum MetaDataBackground {
static let defaultMargin: CGFloat = 4
static let cornerRadius = Thumbnail.cornerRadius - defaultMargin
}
/// System Message text
enum SystemMessageText {
/// Default text style for system messages
static let defaultTextStyle = UIFont.TextStyle.footnote
/// Work / consumer font for system messages
static let workConsumerFont = UIFont.preferredFont(forTextStyle: defaultTextStyle).bold()
/// Configuration for work / consumer info
static let workConsumerSymbolConfiguration = UIImage.SymbolConfiguration(
textStyle: defaultTextStyle,
scale: .large
)
}
/// Metadata information (eg. date and state or duration in call cells)
enum MessageMetadata {
/// Space between label and symbol
static let defaultLabelAndSymbolSpace: CGFloat = 4
/// Inset from leading or trailing end to center of symbol
static let defaultSymbolCenterInset: CGFloat = 6
/// Minimal space between a leading and trailing metadata view
static let minimalInBetweenSpace: CGFloat = 4
/// Text style for labels and symbols
static let textStyle = UIFont.TextStyle.caption1
/// Font for labels
/// Remember to update the font in `fontPointSize` as well
static let font = UIFont.preferredFont(forTextStyle: .caption1)
/// Font point size for `font`
static let fontPointSize = {
/// This must be equal to the font used in `font`
/// We cannot use `font` directly because otherwise the size doesn't change when updating the font size
/// while the app is running
UIFont.preferredFont(forTextStyle: .caption1).pointSize
}
/// Monospaced digit font of same point size as `font`
static let monospacedDigitFont = {
UIFont.monospacedDigitSystemFont(
ofSize: fontPointSize(),
weight: .regular
)
}
/// Configuration for symbol
static let symbolConfiguration = UIImage.SymbolConfiguration(
textStyle: textStyle,
scale: .small
)
}
/// Background for a metadata view
enum MetadataBackground {
/// Default margin for background from rest
static let defaultMargin: CGFloat = 4
/// Corner radius of background
static let cornerRadius = Thumbnail.cornerRadius - defaultMargin
/// Default top and bottom inset of metadata view
static let topAndBottomInset: CGFloat = 2
/// Default leading and trailing inset of metadata view
static let leadingAndTrailingInset: CGFloat = 8
}
enum MentionsView {
static let maxHeight: CGFloat = 250
/// Animation duration for appearing and disappearing
static let animationDuration: CGFloat = 0.25
}
enum UnreadMessageLine {
static let minimalCellHeight = 30.0
static let pillLeftRightTextInset = 8.0
static let pillTopBottomTextInset = 4.0
static let adjustedTopInset = ChatBubble.defaultTopBottomInset
static let adjustedBottomInset = 0.0
static let adjustedGroupTopInset = 0.0
static let adjustedGroupBottomInset = (ChatBubble.defaultGroupTopBottomInset * 0.75)
static let pillRadius = 12.0
static let pillBlurEffectStyle = UIBlurEffect.Style.systemThinMaterial
static let leftRightLineHeight = 2.0
static let leftRightLineRadius = 1.0
static let leftRightLineInnerRoundedCorners = true
static let leftRightLineOuterRoundedCorners = false
static let leftRightLineMaxScreenwidthRatio = 0.125
static let font = UIFont.monospacedDigitSystemFont(
ofSize: UIFont.preferredFont(forTextStyle: .subheadline).pointSize,
weight: .medium
)
/// Maximum time in milliseconds before the unread message line disappears when the user scrolls to the bottom
/// of the chat view
/// Might be shorter than this if the data source applies a snapshot before
static let timeBeforeDisappear = 1500
/// Timeout in seconds before we cancel the `willEnterForegroundCompletion`
static let completionTimeout: Double = 2
}
enum ScrollToBottomButton {
/// Insets left and right to the content view containing the button and the number of unread messages
static let leftRightInsets = 15.0
/// Insets top and bottom to the content view containing the button and the number of unread messages
static let topBottomInsets = 7.5
/// Corner radius of the views on the left side
static let cornerRadius = 5.0
/// Content insets for the glass button
static let glassButtonInsets = 12.0
/// Distance between image and text in glass button
static let glassButtonImageTextPadding = 8.0
/// Distance between the bottom edge of the button and the chat bar
static let distanceToChatBar =
if #available(iOS 26.0, *) {
32.0
}
else {
15.0
}
enum ShowHideAnimation {
static let duration = 0.5
static let delay = 0.25
}
/// Combine debounce in ms
/// Applied before new changes are checked
static let dataUpdateDebounce = 500
}
enum DataSource {
static let typingIndicatorThrottle: TimeInterval = 0.25
static let unreadMessagesSnapshotStateThrottleInMs = 250
/// Delay before the current messages snapshot is processed by the snapshot provider
/// This can be increased to multiple hundreds of ms if we actually do snapshot batching
static let currentMessageSnapshotDelay = 500
/// Duration in s of the custom animation block applied when only the state but not the number of messages
/// changes and we are staying at the bottom of the chat view.
static let animationDuration: TimeInterval = 0.25
}
enum SearchResults {
/// Text style for sender name shown in search results
static let nameTextStyle = UIFont.TextStyle.headline
/// Text style for date and disclosure indicator
static let metadataTextStyle = UIFont.TextStyle.subheadline
/// Spacing between date and disclosure indicator
static let metadataSpacing = 4.0
/// Spacing between name and meta
static let nameAndMetadataSpacing = 8.0
/// Text style for message preview text
static let messagePreviewTextTextStyle = UIFont.TextStyle.body
/// Spacing between name & metadata and message preview
static let verticalSpacing = 4.0
/// Color used for highlighting search text in cells is defined directly in `MarkupParser`
}
enum SearchResultsFetching {
/// Will display a maximum number of `maxItemsToFetch` of each message type in the search results
/// 0 means no limit
static let maxItemsToFetch = 0
/// The number of seconds to wait before starting a new fetch request
static let debounceInputSeconds: RunLoop.SchedulerTimeType.Stride = 0.5
}
enum ScrollCompletionBehavior {
/// Whether to use `scrollToRow(at:at:animated:)` or`UIView.animate` with `animated` always set to false in
/// `scrollToRow(at:at:animated:)`
/// `UIView.animate` seems to be more stable but might have not yet discovered side effects.
static let useCustomViewAnimationBlock = true
static let animationDuration = 0.25
/// Delay in ms before the `scrollCompletion` block is called to signal that the scrolling animation has
/// completed
static let completionBlockCallDelay = 50
}
enum ScrollBehavior {
/// We need some non-zero additional space above the content height of newly added cells when checking
/// whether we can adjust the content offset or must scroll down to avoid glitches
/// You can find a more detailed description of the workaround in
/// `willApplySnapshot(currentDoesIncludeNewestMessage:)` of `ChatViewController`
static let newCellsContentHeightLeeway = 1.0
}
enum EmojiReactions {
enum Animation {
static let defaultHideShowDuration = 0.3
static let defaultDelay = 0.75
static let defaultSpeed = 2.0
}
enum Picker {
// Grid
static let columnSpacing: CGFloat = 8
static let rowSpacing: CGFloat = 0
static let onPressScale: CGFloat = 0.95
// Search
static let initialItemScale: CGFloat = 0.8
// SkinTonePicker
static let minimumLongPressDuration: TimeInterval = 0.5
static let cornerRadius: CGFloat = 14
// SectionToolbar
static let toolbarHeight: CGFloat =
if #available(iOS 26.0, *) {
30
}
else {
60
}
static let horizontalPadding: CGFloat = 16
static let itemHeightMargin: CGFloat = 4
static let yOffsetOnDismissal: CGFloat = 100
// EmojiView
static let minEmojiViewWidth: CGFloat = 50
static let maxEmojiViewWidth: CGFloat = 60
}
enum ContextMenuView {
static let maxFrameWidthMultiplier = 0.7
static let viewPadding = 4.0
static let buttonPadding = 4.0
static let buttonSpacing = 2.0
static let disabledButtonOpacity = 0.3
}
enum CellStackView {
static let itemSpacing = 4.0
static let itemBorderWidth = 1.0
static let itemMinWidth = 20.0
static let itemTopBottomInset = 6.0
static let itemLeadingTrailingInset = 8.0
}
}
}
¤ Dauer der Verarbeitung: 0.11 Sekunden
¤
*© Formatika GbR, Deutschland