mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
4a9cd02fbc
Introduces the GalleryRoomTimelineItem and its typed GalleryItem model (reusing the existing per-message content types), builds it in the RoomTimelineItemFactory and routes it through RoomTimelineItemView. For now it renders as a placeholder; the grid/list media views and the full-screen preview land in follow-up PRs. Co-authored-by: svetoslav.mitov <svetoslav.mitov@sportinno.com>
123 lines
4.2 KiB
Swift
123 lines
4.2 KiB
Swift
//
|
|
// Copyright 2025 Element Creations Ltd.
|
|
// Copyright 2022-2025 New Vector Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
// Please see LICENSE files in the repository root for full details.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
|
|
nonisolated protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol, CustomStringConvertible {
|
|
var timestamp: Date { get }
|
|
var isOutgoing: Bool { get }
|
|
var isEditable: Bool { get }
|
|
var canBeRepliedTo: Bool { get }
|
|
|
|
var sender: TimelineItemSender { get }
|
|
|
|
var body: String { get }
|
|
|
|
var properties: RoomTimelineItemProperties { get }
|
|
}
|
|
|
|
nonisolated extension EventBasedTimelineItemProtocol {
|
|
var description: String {
|
|
"\(String(describing: Self.self)): id: \(id), timestamp: \(timestamp), isOutgoing: \(isOutgoing), properties: \(properties)"
|
|
}
|
|
|
|
var isForwardable: Bool {
|
|
isRemoteMessage && !(self is PollRoomTimelineItem)
|
|
}
|
|
|
|
var isRemoteMessage: Bool {
|
|
id.eventID != nil
|
|
}
|
|
|
|
var isRedacted: Bool {
|
|
self is RedactedRoomTimelineItem
|
|
}
|
|
|
|
var pollIfAvailable: Poll? {
|
|
(self as? PollRoomTimelineItem)?.poll
|
|
}
|
|
|
|
var hasStatusIcon: Bool {
|
|
hasFailedToSend || properties.encryptionAuthenticity != nil || properties.encryptionForwarder != nil
|
|
}
|
|
|
|
var hasFailedToSend: Bool {
|
|
properties.deliveryStatus?.isSendingFailed == true
|
|
}
|
|
|
|
var hasFailedDecryption: Bool {
|
|
self is EncryptedRoomTimelineItem
|
|
}
|
|
|
|
var timelineMenuDescription: String {
|
|
switch self {
|
|
case is VoiceMessageRoomTimelineItem:
|
|
return L10n.commonVoiceMessage
|
|
default:
|
|
return body.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
}
|
|
|
|
/// The inline area to reserve at the trailing edge of the message text so the
|
|
/// bubble's natural size accommodates the timestamp (which is overlaid on the
|
|
/// bubble's bottom-trailing corner). Text wraps around this area: if it fits on
|
|
/// the last line, the timestamp tucks inline; otherwise it sits on a new line.
|
|
/// Sized using the same dynamic-type-aware font as the rendered timestamp.
|
|
var trailingReservedSize: CGSize {
|
|
let textStyle: UIFont.TextStyle = .caption1 // matches compound.bodyXS
|
|
let font = UIFont.preferredFont(forTextStyle: textStyle)
|
|
let textSize = (localizedSendInfo as NSString).size(withAttributes: [.font: font])
|
|
var width = textSize.width
|
|
var height = textSize.height
|
|
if hasStatusIcon {
|
|
// CompoundIcon at .xSmall is 16pt at the base size; scale with the same UIFontMetrics
|
|
// as the icon's `relativeTo: .compound.bodyXS` modifier, plus the 4pt HStack spacing.
|
|
let iconSize = UIFontMetrics(forTextStyle: textStyle).scaledValue(for: 16)
|
|
width += iconSize + 4
|
|
height = max(height, iconSize)
|
|
}
|
|
// Small visual gap between the text content and the overlaid timestamp.
|
|
width += 4
|
|
return CGSize(width: ceil(width), height: ceil(height))
|
|
}
|
|
|
|
/// contains the timestamp and an optional edited localised prefix
|
|
/// example: (edited) 12:17 PM
|
|
var localizedSendInfo: String {
|
|
var start = ""
|
|
if properties.isEdited {
|
|
start = "\(L10n.commonEditedSuffix) "
|
|
}
|
|
return start + timestamp.formattedTime()
|
|
}
|
|
|
|
var isCopyable: Bool {
|
|
guard let messageBasedItem = self as? EventBasedMessageTimelineItemProtocol else {
|
|
return false
|
|
}
|
|
|
|
switch messageBasedItem.contentType {
|
|
case .audio, .file, .image, .video, .location, .voice, .gallery:
|
|
return false
|
|
case .text, .emote, .notice:
|
|
return true
|
|
}
|
|
}
|
|
|
|
var supportsMediaCaption: Bool {
|
|
guard let messageBasedItem = self as? EventBasedMessageTimelineItemProtocol else { return false }
|
|
return messageBasedItem.supportsMediaCaption
|
|
}
|
|
|
|
var hasMediaCaption: Bool {
|
|
guard let messageBasedItem = self as? EventBasedMessageTimelineItemProtocol else { return false }
|
|
return messageBasedItem.hasMediaCaption
|
|
}
|
|
}
|