Files
simplex-chat/apps/ios/Shared/Views/Chat/ChatItemView.swift
2024-01-24 08:21:04 -08:00

274 lines
13 KiB
Swift

//
// ChatItemView.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 30/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct ChatItemView: View {
@ObservedObject var chat: Chat
var chatItem: ChatItem
var maxWidth: CGFloat = .infinity
@State var scrollProxy: ScrollViewProxy? = nil
@Binding var revealed: Bool
@Binding var allowMenu: Bool
@Binding var audioPlayer: AudioPlayer?
@Binding var playbackState: VoiceMessagePlaybackState
@Binding var playbackTime: TimeInterval?
init(
chat: Chat,
chatItem: ChatItem,
showMember: Bool = false,
maxWidth: CGFloat = .infinity,
scrollProxy: ScrollViewProxy? = nil,
revealed: Binding<Bool>,
allowMenu: Binding<Bool> = .constant(false),
audioPlayer: Binding<AudioPlayer?> = .constant(nil),
playbackState: Binding<VoiceMessagePlaybackState> = .constant(.noPlayback),
playbackTime: Binding<TimeInterval?> = .constant(nil)
) {
self.chat = chat
self.chatItem = chatItem
self.maxWidth = maxWidth
_scrollProxy = .init(initialValue: scrollProxy)
_revealed = revealed
_allowMenu = allowMenu
_audioPlayer = audioPlayer
_playbackState = playbackState
_playbackTime = playbackTime
}
var body: some View {
let ci = chatItem
if chatItem.meta.itemDeleted != nil && (!revealed || chatItem.isDeletedContent) {
MarkedDeletedItemView(chat: chat, chatItem: chatItem, revealed: $revealed)
} else if ci.quotedItem == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive {
if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) {
EmojiItemView(chat: chat, chatItem: ci)
} else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent {
CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, allowMenu: $allowMenu)
} else if ci.content.msgContent == nil {
ChatItemContentView(chat: chat, chatItem: chatItem, revealed: $revealed, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
} else {
framedItemView()
}
} else {
framedItemView()
}
}
private func framedItemView() -> some View {
FramedItemView(chat: chat, chatItem: chatItem, revealed: $revealed, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
}
}
struct ChatItemContentView<Content: View>: View {
@EnvironmentObject var chatModel: ChatModel
@ObservedObject var chat: Chat
var chatItem: ChatItem
@Binding var revealed: Bool
var msgContentView: () -> Content
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
var body: some View {
switch chatItem.content {
case .sndMsgContent: msgContentView()
case .rcvMsgContent: msgContentView()
case .sndDeleted: deletedItemView()
case .rcvDeleted: deletedItemView()
case let .sndCall(status, duration): callItemView(status, duration)
case let .rcvCall(status, duration): callItemView(status, duration)
case let .rcvIntegrityError(msgError):
if developerTools {
IntegrityErrorItemView(chat: chat, msgError: msgError, chatItem: chatItem)
} else {
ZStack {}
}
case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(chat: chat, msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem)
case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
case .rcvDirectEvent: eventItemView()
case .rcvGroupEvent(.memberCreatedContact): CIMemberCreatedContactView(chatItem: chatItem)
case .rcvGroupEvent: eventItemView()
case .sndGroupEvent: eventItemView()
case .rcvConnEvent: eventItemView()
case .sndConnEvent: eventItemView()
case let .rcvChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor)
case let .sndChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor)
case let .rcvChatPreference(feature, allowed, param):
CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param)
case let .sndChatPreference(feature, _, _):
CIChatFeatureView(chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: .secondary)
case let .rcvGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor)
case let .sndGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor)
case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red)
case let .rcvGroupFeatureRejected(feature): chatFeatureView(feature, .red)
case .sndModerated: deletedItemView()
case .rcvModerated: deletedItemView()
case .rcvBlocked: deletedItemView()
case let .invalidJSON(json): CIInvalidJSONView(json: json)
}
}
private func deletedItemView() -> some View {
DeletedItemView(chat: chat, chatItem: chatItem)
}
private func callItemView(_ status: CICallStatus, _ duration: Int) -> some View {
CICallItemView(chat: chat, chatItem: chatItem, status: status, duration: duration)
}
private func groupInvitationItemView(_ groupInvitation: CIGroupInvitation, _ memberRole: GroupMemberRole) -> some View {
CIGroupInvitationView(chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole, chatIncognito: chat.chatInfo.incognito)
}
private func eventItemView() -> some View {
return CIEventView(eventText: eventItemViewText())
}
private func eventItemViewText() -> Text {
if !revealed, let t = mergedGroupEventText {
return chatEventText(t + Text(" ") + chatItem.timestampText)
} else if let member = chatItem.memberDisplayName {
return Text(member + " ")
.font(.caption)
.foregroundColor(.secondary)
.fontWeight(.light)
+ chatEventText(chatItem)
} else {
return chatEventText(chatItem)
}
}
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
CIChatFeatureView(chatItem: chatItem, revealed: $revealed, feature: feature, iconColor: iconColor)
}
private var mergedGroupEventText: Text? {
let (count, ns, lastNonConnectedEvent) = chatModel.getConnectedMemberNames(chatItem)
let members: LocalizedStringKey =
switch ns.count {
case 1: "\(ns[0]) connected"
case 2: "\(ns[0]) and \(ns[1]) connected"
case 3: "\(ns[0] + ", " + ns[1]) and \(ns[2]) connected"
default:
ns.count > 3
? "\(ns[0]), \(ns[1]) and \(ns.count - 2) other members connected"
: ""
}
return if count <= 1 {
nil
} else if let last = lastNonConnectedEvent {
Text(last) + Text(" ") + Text("and \(count - ns.count) other events")
} else if ns.count == 0 {
Text("\(count) group events")
} else if count > ns.count {
Text(members) + Text(" ") + Text("and \(count - ns.count) other events")
} else {
Text(members)
}
}
}
func chatEventText(_ text: Text) -> Text {
text
.font(.caption)
.foregroundColor(.secondary)
.fontWeight(.light)
}
func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text) -> Text {
chatEventText(Text(eventText) + Text(" ") + ts)
}
func chatEventText(_ ci: ChatItem) -> Text {
chatEventText("\(ci.content.text)", ci.timestampText)
}
struct ChatItemView_Previews: PreviewProvider {
static var previews: some View {
Group{
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(false))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true))
}
.previewLayout(.fixed(width: 360, height: 70))
.environmentObject(Chat.sampleData)
}
}
struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
static var previews: some View {
let ciFeatureContent = CIContent.rcvChatFeature(feature: .fullDelete, enabled: FeatureEnabled(forUser: false, forContact: false), param: nil)
Group{
ChatItemView(
chat: Chat.sampleData,
chatItem: ChatItem(
chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
content: .rcvIntegrityError(msgError: .msgSkipped(fromMsgId: 1, toMsgId: 2)),
quotedItem: nil,
file: nil
),
revealed: Binding.constant(true)
)
ChatItemView(
chat: Chat.sampleData,
chatItem: ChatItem(
chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead),
content: .rcvDecryptionError(msgDecryptError: .ratchetHeader, msgCount: 2),
quotedItem: nil,
file: nil
),
revealed: Binding.constant(true)
)
ChatItemView(
chat: Chat.sampleData,
chatItem: ChatItem(
chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, "received invitation to join group team as admin", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
content: .rcvGroupInvitation(groupInvitation: CIGroupInvitation.getSample(status: .pending), memberRole: .admin),
quotedItem: nil,
file: nil
),
revealed: Binding.constant(true)
)
ChatItemView(
chat: Chat.sampleData,
chatItem: ChatItem(
chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, "group event text", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
content: .rcvGroupEvent(rcvGroupEvent: .memberAdded(groupMemberId: 1, profile: Profile.sampleData)),
quotedItem: nil,
file: nil
),
revealed: Binding.constant(true)
)
ChatItemView(
chat: Chat.sampleData,
chatItem: ChatItem(
chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, ciFeatureContent.text, .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
content: ciFeatureContent,
quotedItem: nil,
file: nil
),
revealed: Binding.constant(true)
)
}
.previewLayout(.fixed(width: 360, height: 70))
.environmentObject(Chat.sampleData)
}
}