mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-11 13:57:42 +00:00
* plan: UI improvements to improve LLM usage via SimpleX * core, ui: types for chat layout * core, ui: settings for alternative terminal-like chat layout for managing LLMs or other service bots * plan: message priority * fixes * query plan * update plans * remove silent messages plan * rename migrations * implement layout * amend * fix * fix * fix * improve * time on the right, sender name in blue * no avatar in direct chats * better previews * update, fixes * update * time gap * changes * ui: terminal layout line heights, gaps and time placement * ui: simplify terminal meta layout, correct its docs and tests * ui: right-to-left emoji edge in terminal rows, time placement tests * android, desktop: raise Kotlin compiler memory to 4 GB * ui: terminal captions as follow-up text, classic event separators * ui: terminal media edge and overlay time, plan decisions * ui: simplify terminal row separation, correct doc line references * ui: keep the media time inside terminal rows, restore caption conditions * ui: terminal heading capitals at large font scales, idle live message time * ui: rename the classic voice-with-text flag, correct the meta copy docs * ui: smaller terminal layout diff, sender gap and meta names * ui: terminal row as tall as the media time * docs: remove the chat layout section from the bots readme * docs: shorten the terminal layout section of the chat view * core: address settings API as on master, /auto_accept reads the stored layout * ui: smaller terminal headings * ui: classic sender rows and avatar rule as on master * ui: classic reply quote text colour as on master * ios: keep the chat layout setting with other UI settings * ui: go-to button at the end of terminal rows, no gutter in direct chats * ui: forwarded arrow on the header line of terminal rows, above the time * ui: forwarded arrow centred above the time line of terminal rows * ios: terminal chat layout, chosen per app, profile and chat * ios: event time reserve of three spaces, app layout picker as Kotlin * ios: spec and plan of the terminal chat layout * ios: classic message text at 87% in dark themes, as terminal rows * ios: terminal gaps scaled by the body size, 17/16 of Kotlin values * ios: terminal quote sender at 13.5pt, thumbnail at the item end * ios: product docs of the terminal layout, plans updated for iOS * ios: chat scenes rendered to PNG by a unit test in the simulator CI * ios: snapshots rendered by the smoke test script, workflow unchanged * ios: terminal names at 13.5 x 17/16 pt, as the gaps * ios: Markdown views at the 87% message colour in dark themes * ios: terminal quote thumbnail 12pt after the quote text or the bar * ios: terminal rule-only message widened by a wider quote or header * ios: member avatars scaled with the text size, as in the chat list * ios: snapshot rows at their own height, as list cells, so text wraps * ios: member avatars back at 34pt, for the snapshot before scaling * ios: member avatars scaled with the text size, as in the chat list * ui: terminal quote thumbnail 12dp after the quote text or the bar * ios: classic group bubble width follows the scaled member avatar * ci: ios-sim README describes the snapshots rendered after the smoke test * ios: scratch, padding variants for the I7 and I8 renders, to revert * ios: revert the scratch padding variants after their renders * ui: contrast of the voice message button in terminal rows * ios: scratch, spacing variants and list margins to render, to revert * ios: revert the scratch spacing variants after their renders * ios: snapshots at a fixed date, in UTC and the en_US locale * ios: snapshots drawn as 8-bit sRGB without alpha * ci: compare iOS snapshots with the references in the repository * ci: record and check iOS snapshot references on GitHub's runner * ios: scratch, list margins on the remaining screens, to revert * ios: revert the scratch list margins after their measurement * ci: snapshot scripts executable, as the other CI scripts * ios: scratch, voice circle variants in light themes, to revert * ios: revert the scratch voice circle variants * ios: one 12pt padding at the edges and after avatars in terminal rows * ios: classic group sender name on the bubble edge * ui: one 12dp padding at the edges and after avatars in terminal rows * ios: one heading scale in both chat layouts * ios: snapshot references of the chat scenes * docs: leave the headings and classic lines of the plan to ep/classic-bubbles * ios: declare terminal layout functions inside types * ios: keep layout constants at file scope * ios: keep file-scope functions, move only extension members into types * ios: snapshot tests record row heights at first layout and the appearance layout switch * ios: appearance preview follows the chat layout setting at once * ios: snapshot test measures first-layout rows by SwiftUI and by cells * ios: chat rows measured at the chat width when they first appear * ios: snapshot test checks that chat rows keep their first height after a reload * ios, ci: references of terminal and classic chat scenes, dark message text * ios: message text at 95% in dark themes * ios, ci: references with message text at 95% in dark themes * ios: media in terminal rows end at the time's right inset * ios, ci: references with terminal media at the time's right inset --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
417 lines
19 KiB
Swift
417 lines
19 KiB
Swift
//
|
|
// ChatItemView.swift
|
|
// SimpleX
|
|
//
|
|
// Created by Evgeny Poberezkin on 30/01/2022.
|
|
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
//
|
|
|
|
import SwiftUI
|
|
import SimpleXChat
|
|
|
|
extension EnvironmentValues {
|
|
struct ShowTimestamp: EnvironmentKey {
|
|
static let defaultValue: Bool = true
|
|
}
|
|
|
|
struct Revealed: EnvironmentKey {
|
|
static let defaultValue: Bool = true
|
|
}
|
|
|
|
struct ContainerBackground: EnvironmentKey {
|
|
static let defaultValue: UIColor = .clear
|
|
}
|
|
|
|
var showTimestamp: Bool {
|
|
get { self[ShowTimestamp.self] }
|
|
set { self[ShowTimestamp.self] = newValue }
|
|
}
|
|
|
|
var revealed: Bool {
|
|
get { self[Revealed.self] }
|
|
set { self[Revealed.self] = newValue }
|
|
}
|
|
|
|
var containerBackground: UIColor {
|
|
get { self[ContainerBackground.self] }
|
|
set { self[ContainerBackground.self] = newValue }
|
|
}
|
|
}
|
|
|
|
// Spec: spec/client/chat-view.md#ChatItemView
|
|
struct ChatItemView: View {
|
|
@ObservedObject var chat: Chat
|
|
@ObservedObject var im: ItemsModel
|
|
@EnvironmentObject var theme: AppTheme
|
|
@Environment(\.showTimestamp) var showTimestamp: Bool
|
|
@Environment(\.revealed) var revealed: Bool
|
|
var chatItem: ChatItem
|
|
var scrollToItem: (ChatItem.ID) -> Void
|
|
@Binding var scrollToItemId: ChatItem.ID?
|
|
var maxWidth: CGFloat = .infinity
|
|
@Binding var allowMenu: Bool
|
|
var endButton: AnyView?
|
|
|
|
init(
|
|
chat: Chat,
|
|
im: ItemsModel,
|
|
chatItem: ChatItem,
|
|
scrollToItem: @escaping (ChatItem.ID) -> Void,
|
|
scrollToItemId: Binding<ChatItem.ID?> = .constant(nil),
|
|
showMember: Bool = false,
|
|
maxWidth: CGFloat = .infinity,
|
|
allowMenu: Binding<Bool> = .constant(false),
|
|
endButton: AnyView? = nil
|
|
) {
|
|
self.chat = chat
|
|
self.im = im
|
|
self.chatItem = chatItem
|
|
self.scrollToItem = scrollToItem
|
|
_scrollToItemId = scrollToItemId
|
|
self.maxWidth = maxWidth
|
|
_allowMenu = allowMenu
|
|
self.endButton = endButton
|
|
}
|
|
|
|
var body: some View {
|
|
let ci = chatItem
|
|
if chatItem.meta.itemDeleted != nil && (!revealed || chatItem.isDeletedContent) {
|
|
MarkedDeletedItemView(chat: chat, im: im, chatItem: chatItem)
|
|
} else if ci.quotedItem == nil && ci.meta.itemForwarded == 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, allowMenu: $allowMenu)
|
|
} else if ci.content.msgContent == nil {
|
|
ChatItemContentView(chat: chat, im: im, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
|
|
} else {
|
|
framedItemView()
|
|
}
|
|
} else {
|
|
framedItemView()
|
|
}
|
|
}
|
|
|
|
private func framedItemView() -> some View {
|
|
let preview = chatItem.content.msgContent
|
|
.flatMap {
|
|
switch $0 {
|
|
case let .image(_, image): image
|
|
case let .video(_, image, _): image
|
|
default: nil
|
|
}
|
|
}
|
|
.flatMap { imageFromBase64($0) }
|
|
let adjustedMaxWidth = {
|
|
if let preview, preview.size.width <= preview.size.height {
|
|
maxWidth * 0.75
|
|
} else {
|
|
maxWidth
|
|
}
|
|
}()
|
|
return FramedItemView(
|
|
chat: chat,
|
|
im: im,
|
|
chatItem: chatItem,
|
|
scrollToItem: scrollToItem,
|
|
scrollToItemId: $scrollToItemId,
|
|
preview: preview,
|
|
maxWidth: maxWidth,
|
|
imgWidth: adjustedMaxWidth,
|
|
videoWidth: adjustedMaxWidth,
|
|
allowMenu: $allowMenu,
|
|
endButton: endButton
|
|
)
|
|
}
|
|
}
|
|
|
|
struct ChatItemContentView<Content: View>: View {
|
|
@EnvironmentObject var chatModel: ChatModel
|
|
@EnvironmentObject var theme: AppTheme
|
|
@Environment(\.revealed) var revealed: Bool
|
|
@ObservedObject var chat: Chat
|
|
@ObservedObject var im: ItemsModel
|
|
var chatItem: ChatItem
|
|
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 .rcvMsgError(rcvMsgError): RcvMsgErrorItemView(chat: chat, rcvMsgError: rcvMsgError, chatItem: chatItem)
|
|
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(.newMemberPendingReview): CIEventView(eventText: pendingReviewEventItemText())
|
|
case .rcvGroupEvent: eventItemView()
|
|
case .sndGroupEvent(.userPendingReview): CIEventView(eventText: pendingReviewEventItemText())
|
|
case .sndGroupEvent: eventItemView()
|
|
case .rcvConnEvent: eventItemView()
|
|
case .sndConnEvent: eventItemView()
|
|
case let .rcvChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor(theme.colors.secondary))
|
|
case let .sndChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor(theme.colors.secondary))
|
|
case let .rcvChatPreference(feature, allowed, param):
|
|
CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param)
|
|
case let .sndChatPreference(feature, _, _):
|
|
CIChatFeatureView(chat: chat, im: im, chatItem: chatItem, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary)
|
|
case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary))
|
|
case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary))
|
|
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 .sndDirectE2EEInfo(e2eeInfo): CIEventView(eventText: directE2EEInfoText(e2eeInfo))
|
|
case let .rcvDirectE2EEInfo(e2eeInfo): CIEventView(eventText: directE2EEInfoText(e2eeInfo))
|
|
case let .sndGroupE2EEInfo(e2eeInfo): CIEventView(eventText: groupE2EEInfoText(e2eeInfo))
|
|
case let .rcvGroupE2EEInfo(e2eeInfo): CIEventView(eventText: groupE2EEInfoText(e2eeInfo))
|
|
case .chatBanner: EmptyView()
|
|
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(chat: chat, chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole, chatIncognito: chat.chatInfo.incognito)
|
|
}
|
|
|
|
private func eventItemView() -> some View {
|
|
ChatEventWithMeta(chatItem: chatItem) { ts in
|
|
CIEventView(eventText: eventItemViewText(theme.colors.secondary, ts))
|
|
}
|
|
}
|
|
|
|
private func pendingReviewEventItemText() -> Text {
|
|
Text(chatItem.content.text(isChannel: chat.chatInfo.isChannel))
|
|
.font(.caption)
|
|
.foregroundColor(theme.colors.secondary)
|
|
.fontWeight(.bold)
|
|
}
|
|
|
|
private func eventItemViewText(_ secondaryColor: Color, _ ts: Text) -> Text {
|
|
if !revealed, let t = mergedGroupEventText {
|
|
return chatEventText(t + textSpace + ts, secondaryColor)
|
|
} else if let member = chatItem.memberDisplayName {
|
|
return Text(member + " ")
|
|
.font(.caption)
|
|
.foregroundColor(secondaryColor)
|
|
.fontWeight(.light)
|
|
+ chatEventText(chatItem, secondaryColor, isChannel: chat.chatInfo.isChannel, ts: ts)
|
|
} else {
|
|
return chatEventText(chatItem, secondaryColor, isChannel: chat.chatInfo.isChannel, ts: ts)
|
|
}
|
|
}
|
|
|
|
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
|
|
CIChatFeatureView(chat: chat, im: im, chatItem: chatItem, feature: feature, iconColor: iconColor)
|
|
}
|
|
|
|
private var mergedGroupEventText: Text? {
|
|
let (count, ns) = 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 ns.count == 0 {
|
|
chat.chatInfo.isChannel ? Text("\(count) channel events") : Text("\(count) group events")
|
|
} else if count > ns.count {
|
|
Text(members) + textSpace + Text("and \(count - ns.count) other events")
|
|
} else {
|
|
Text(members)
|
|
}
|
|
}
|
|
|
|
private func directE2EEInfoText(_ info: E2EEInfo) -> Text {
|
|
if let pqEnabled = info.pqEnabled {
|
|
pqEnabled
|
|
? e2eeInfoText("Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.")
|
|
: e2eeInfoNoPQText()
|
|
} else {
|
|
e2eeInfoText("Messages are protected by **end-to-end encryption**.")
|
|
}
|
|
}
|
|
|
|
private func e2eeInfoNoPQText() -> Text {
|
|
e2eeInfoText("Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.")
|
|
}
|
|
|
|
private func groupE2EEInfoText(_ info: E2EEInfo) -> Text {
|
|
info.public == true
|
|
? e2eeInfoText("Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages.")
|
|
: e2eeInfoNoPQText()
|
|
}
|
|
|
|
private func e2eeInfoText(_ s: LocalizedStringKey) -> Text {
|
|
Text(s)
|
|
.font(.caption)
|
|
.foregroundColor(theme.colors.secondary)
|
|
.fontWeight(.light)
|
|
}
|
|
}
|
|
|
|
func chatEventText(_ text: Text, _ secondaryColor: Color) -> Text {
|
|
text
|
|
.font(.caption)
|
|
.foregroundColor(secondaryColor)
|
|
.fontWeight(.light)
|
|
}
|
|
|
|
func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text, _ secondaryColor: Color) -> Text {
|
|
chatEventText(Text(eventText) + textSpace + ts, secondaryColor)
|
|
}
|
|
|
|
func chatEventText(_ ci: ChatItem, _ secondaryColor: Color, isChannel: Bool = false, ts: Text) -> Text {
|
|
chatEventText("\(ci.content.text(isChannel: isChannel))", ts, secondaryColor)
|
|
}
|
|
|
|
func previewDated(_ chatItem: ChatItem) -> ChatItem {
|
|
var ci = chatItem
|
|
ci.meta.itemTs = previewDate
|
|
ci.meta.createdAt = previewDate
|
|
ci.meta.updatedAt = previewDate
|
|
return ci
|
|
}
|
|
|
|
struct ChatItemView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
let im = ItemsModel.shared
|
|
Group{
|
|
let ci1 = ChatItem.getSample(1, .directSnd, previewDate, "hello")
|
|
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ci1, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)).modifier(ChatItemClipped(ci1, tailVisible: true))
|
|
let ci2 = ChatItem.getSample(2, .directRcv, previewDate, "hello there too")
|
|
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ci2, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)).modifier(ChatItemClipped(ci2, tailVisible: true))
|
|
let ci3 = ChatItem.getSample(1, .directSnd, previewDate, "🙂")
|
|
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ci3, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)).modifier(ChatItemClipped(ci3, tailVisible: true))
|
|
let ci4 = ChatItem.getSample(2, .directRcv, previewDate, "🙂🙂🙂🙂🙂")
|
|
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ci4, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)).modifier(ChatItemClipped(ci4, tailVisible: true))
|
|
let ci5 = ChatItem.getSample(2, .directRcv, previewDate, "🙂🙂🙂🙂🙂🙂")
|
|
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ci5, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)).modifier(ChatItemClipped(ci5, tailVisible: true))
|
|
let ci6 = ChatItem.getDeletedContentSample(1, previewDate)
|
|
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ci6, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)).modifier(ChatItemClipped(ci6, tailVisible: true))
|
|
let ci7 = ChatItem.getSample(1, .directSnd, previewDate, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: previewDate))
|
|
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ci7, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)).modifier(ChatItemClipped(ci7, tailVisible: true))
|
|
let ci8 = ChatItem.getSample(1, .directSnd, previewDate, "🙂", .sndSent(sndProgress: .complete), itemLive: true)
|
|
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ci8, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)).environment(\.revealed, true).modifier(ChatItemClipped(ci8, tailVisible: true))
|
|
let ci9 = ChatItem.getSample(1, .directSnd, previewDate, "hello", .sndSent(sndProgress: .complete), itemLive: true)
|
|
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ci9, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)).environment(\.revealed, true).modifier(ChatItemClipped(ci9, tailVisible: true))
|
|
}
|
|
.environment(\.revealed, false)
|
|
.previewLayout(.fixed(width: 360, height: 70))
|
|
.environmentObject(Chat.sampleData)
|
|
}
|
|
}
|
|
|
|
struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
let im = ItemsModel.shared
|
|
let ciFeatureContent = CIContent.rcvChatFeature(feature: .fullDelete, enabled: FeatureEnabled(forUser: false, forContact: false), param: nil)
|
|
Group{
|
|
let ci1 = ChatItem(
|
|
chatDir: .directRcv,
|
|
meta: CIMeta.getSample(1, previewDate, "1 skipped message", .rcvRead, itemDeleted: .deleted(deletedTs: previewDate)),
|
|
content: .rcvIntegrityError(msgError: .msgSkipped(fromMsgId: 1, toMsgId: 2)),
|
|
quotedItem: nil,
|
|
file: nil
|
|
)
|
|
ChatItemView(
|
|
chat: Chat.sampleData,
|
|
im: im,
|
|
chatItem: ci1,
|
|
scrollToItem: { _ in },
|
|
scrollToItemId: Binding.constant(nil)
|
|
)
|
|
.modifier(ChatItemClipped(ci1, tailVisible: true))
|
|
let ci2 = ChatItem(
|
|
chatDir: .directRcv,
|
|
meta: CIMeta.getSample(1, previewDate, "1 skipped message", .rcvRead),
|
|
content: .rcvDecryptionError(msgDecryptError: .ratchetHeader, msgCount: 2),
|
|
quotedItem: nil,
|
|
file: nil
|
|
)
|
|
ChatItemView(
|
|
chat: Chat.sampleData,
|
|
im: im,
|
|
chatItem: ci2,
|
|
scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)
|
|
)
|
|
.modifier(ChatItemClipped(ci2, tailVisible: true))
|
|
let ci3 = ChatItem(
|
|
chatDir: .directRcv,
|
|
meta: CIMeta.getSample(1, previewDate, "received invitation to join group team as admin", .rcvRead, itemDeleted: .deleted(deletedTs: previewDate)),
|
|
content: .rcvGroupInvitation(groupInvitation: CIGroupInvitation.getSample(status: .pending), memberRole: .admin),
|
|
quotedItem: nil,
|
|
file: nil
|
|
)
|
|
ChatItemView(
|
|
chat: Chat.sampleData,
|
|
im: im,
|
|
chatItem: ci3,
|
|
scrollToItem: { _ in },
|
|
scrollToItemId: Binding.constant(nil)
|
|
)
|
|
.modifier(ChatItemClipped(ci3, tailVisible: true))
|
|
let ci4 = ChatItem(
|
|
chatDir: .directRcv,
|
|
meta: CIMeta.getSample(1, previewDate, "group event text", .rcvRead, itemDeleted: .deleted(deletedTs: previewDate)),
|
|
content: .rcvGroupEvent(rcvGroupEvent: .memberAdded(groupMemberId: 1, profile: Profile.sampleData)),
|
|
quotedItem: nil,
|
|
file: nil
|
|
)
|
|
ChatItemView(
|
|
chat: Chat.sampleData,
|
|
im: im,
|
|
chatItem: ci4,
|
|
scrollToItem: { _ in },
|
|
scrollToItemId: Binding.constant(nil)
|
|
)
|
|
.modifier(ChatItemClipped(ci4, tailVisible: true))
|
|
let ci5 = ChatItem(
|
|
chatDir: .directRcv,
|
|
meta: CIMeta.getSample(1, previewDate, ciFeatureContent.text, .rcvRead, itemDeleted: .deleted(deletedTs: previewDate)),
|
|
content: ciFeatureContent,
|
|
quotedItem: nil,
|
|
file: nil
|
|
)
|
|
ChatItemView(
|
|
chat: Chat.sampleData,
|
|
im: im,
|
|
chatItem: ci5,
|
|
scrollToItem: { _ in },
|
|
scrollToItemId: Binding.constant(nil)
|
|
)
|
|
.modifier(ChatItemClipped(ci5, tailVisible: true))
|
|
}
|
|
.environment(\.revealed, true)
|
|
.previewLayout(.fixed(width: 360, height: 70))
|
|
.environmentObject(Chat.sampleData)
|
|
}
|
|
}
|