updated up
This commit is contained in:
1 parent
1e2670903f
commit
cf2bda9c54
6 files changed
+173
-87
No files matched your search
@@ -170,53 +170,47 @@ struct ChatView: View {
|
||||
}
|
||||
} else {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
HStack {
|
||||
AvatarView(
|
||||
conversation: liveConversation,
|
||||
imageData: model.avatarData(for: conversation.jid),
|
||||
size: 28
|
||||
).padding(4)
|
||||
if !conversation.isGroup {
|
||||
Menu {
|
||||
Button {
|
||||
Task {
|
||||
await model.startCall(
|
||||
to: conversation.jid,
|
||||
withVideo: false
|
||||
)
|
||||
}
|
||||
} label: {
|
||||
Label("Аудиозвонок", systemImage: "phone")
|
||||
if !conversation.isGroup {
|
||||
Menu {
|
||||
Button {
|
||||
Task {
|
||||
await model.startCall(
|
||||
to: conversation.jid,
|
||||
withVideo: false
|
||||
)
|
||||
}
|
||||
.disabled(!canStartCall)
|
||||
.help("Аудиозвонок")
|
||||
|
||||
Button {
|
||||
Task {
|
||||
await model.startCall(
|
||||
to: conversation.jid,
|
||||
withVideo: true
|
||||
)
|
||||
}
|
||||
} label: {
|
||||
Label("Видеозвонок", systemImage: "video")
|
||||
}
|
||||
.disabled(!canStartCall)
|
||||
.help("Видеозвонок")
|
||||
encryptionMenu
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
Label("Аудиозвонок", systemImage: "phone")
|
||||
}
|
||||
.disabled(!canStartCall)
|
||||
}
|
||||
if conversation.isGroup {
|
||||
.help("Аудиозвонок")
|
||||
|
||||
Button {
|
||||
showingGroupInfo = true
|
||||
Task {
|
||||
await model.startCall(
|
||||
to: conversation.jid,
|
||||
withVideo: true
|
||||
)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "person.3")
|
||||
Label("Видеозвонок", systemImage: "video")
|
||||
}
|
||||
.help("Информация о группе")
|
||||
.disabled(!canStartCall)
|
||||
.help("Видеозвонок")
|
||||
encryptionMenu
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
}
|
||||
.menuIndicator(.hidden)
|
||||
.disabled(!canStartCall)
|
||||
}
|
||||
if conversation.isGroup {
|
||||
Button {
|
||||
showingGroupInfo = true
|
||||
} label: {
|
||||
Image(systemName: "person.3")
|
||||
}
|
||||
.help("Информация о группе")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -856,7 +850,7 @@ struct ChatView: View {
|
||||
canUseComposer ? Color.accentColor : Color.secondary
|
||||
)
|
||||
.frame(width: 42, height: 42)
|
||||
// .background(.ultraThinMaterial, in: Circle())
|
||||
// .background(.ultraThinMaterial, in: Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(!canUseComposer)
|
||||
@@ -995,24 +989,7 @@ struct ChatView: View {
|
||||
}
|
||||
|
||||
private var chatNavigationTitle: some View {
|
||||
VStack(spacing: 0) {
|
||||
Text(liveConversation.displayName)
|
||||
.font(.headline)
|
||||
.lineLimit(1)
|
||||
if let typingText = model.typingText(for: liveConversation) {
|
||||
Text(typingText)
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundStyle(.tint)
|
||||
.lineLimit(1)
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
.animation(
|
||||
.easeInOut(duration: 0.16),
|
||||
value: model.typingText(for: liveConversation)
|
||||
)
|
||||
.accessibilityElement(children: .combine)
|
||||
.padding()
|
||||
ChatNavigationTitle(model: model, conversation: conversation)
|
||||
}
|
||||
|
||||
private var canSendToConversation: Bool {
|
||||
@@ -2653,3 +2630,69 @@ private func writePickedImageData(_ data: Data) throws -> URL {
|
||||
#Preview {
|
||||
PreviewSupport.chatPreview()
|
||||
}
|
||||
|
||||
/// Extracted chat navigation title: avatar, display name and the transient
|
||||
/// «печатает…» line. Kept as its own view so its layout can be previewed on
|
||||
/// its own without opening a full chat.
|
||||
private struct ChatNavigationTitle: View {
|
||||
@ObservedObject var model: AppModel
|
||||
let conversation: Conversation
|
||||
|
||||
private var liveConversation: Conversation {
|
||||
model.conversations.first(where: { $0.jid == conversation.jid })
|
||||
?? conversation
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
} label: {
|
||||
titleContent
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private var titleContent: some View {
|
||||
HStack {
|
||||
AvatarView(
|
||||
conversation: liveConversation,
|
||||
imageData: model.avatarData(for: conversation.jid),
|
||||
size: 25
|
||||
)
|
||||
.padding(4)
|
||||
VStack(spacing: 0) {
|
||||
Text(liveConversation.displayName)
|
||||
.font(.headline)
|
||||
.lineLimit(1)
|
||||
if let typingText = model.typingText(for: liveConversation) {
|
||||
Text(typingText)
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundStyle(.tint)
|
||||
.lineLimit(1)
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
.padding(.top, 5)
|
||||
.padding(.bottom, 5)
|
||||
.padding(.trailing, 15)
|
||||
.animation(
|
||||
.easeInOut(duration: 0.16),
|
||||
value: model.typingText(for: liveConversation)
|
||||
)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
#if os(iOS)
|
||||
.glassEffect()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Заголовок чата") {
|
||||
ChatNavigationTitle(
|
||||
model: PreviewSupport.model,
|
||||
conversation: PreviewSupport.conversation(
|
||||
displayName: "Алиса",
|
||||
isOnline: true
|
||||
)
|
||||
)
|
||||
.padding()
|
||||
}
|
||||
@@ -37,7 +37,6 @@ struct MessageBubble: View {
|
||||
.offset(x: replySwipeOffset)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(isSelected ? Color.accentColor.opacity(0.1) : Color.clear)
|
||||
.contentShape(Rectangle())
|
||||
.accessibilityIdentifier("bubble-\(message.clientID)")
|
||||
.onTapGesture {
|
||||
@@ -48,10 +47,6 @@ struct MessageBubble: View {
|
||||
.vertical,
|
||||
message.kind == .photo || message.kind == .video ? 7 : 2
|
||||
)
|
||||
.simultaneousGesture(
|
||||
replySwipeGesture,
|
||||
including: !isSelectionMode && message.canBeRepliedTo ? .gesture : .none
|
||||
)
|
||||
.onChange(of: isSelectionMode) { _, selecting in
|
||||
guard selecting else { return }
|
||||
replySwipeOffset = 0
|
||||
@@ -175,6 +170,7 @@ struct MessageBubble: View {
|
||||
)
|
||||
.font(.system(size: 19, weight: .semibold))
|
||||
.foregroundStyle(Color.accentColor)
|
||||
.scaleEffect(x: replySwipeOffset > 0 ? -1 : 1, y: 1)
|
||||
.scaleEffect(
|
||||
CGFloat(0.8) + CGFloat(0.2) * MessageReplySwipePolicy.progress(for: replySwipeOffset)
|
||||
)
|
||||
@@ -232,7 +228,7 @@ struct MessageBubble: View {
|
||||
.onEnded { value in
|
||||
// The lock is a hint, not a requirement: a fast flick may
|
||||
// deliver no intermediate frame that passes canLock, yet its
|
||||
// end state is still a clear leftward swipe. scrollOwnsGesture
|
||||
// end state is still a clear horizontal swipe. scrollOwnsGesture
|
||||
// alone protects the timeline from reply activation.
|
||||
let shouldReply =
|
||||
!scrollOwnsGesture
|
||||
@@ -277,7 +273,15 @@ struct MessageBubble: View {
|
||||
.foregroundStyle(senderColor)
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
// The drag lives on the bubble itself so a swipe can only start
|
||||
// a reply when it begins on the bubble — not on the empty
|
||||
// space beside it, the sender name, or the metadata row.
|
||||
messageContent
|
||||
.contentShape(Rectangle())
|
||||
.simultaneousGesture(
|
||||
replySwipeGesture,
|
||||
including: !isSelectionMode && message.canBeRepliedTo ? .gesture : .none
|
||||
)
|
||||
|
||||
if !reactionSummaries.isEmpty {
|
||||
reactionStrip
|
||||
|
||||
@@ -71,12 +71,13 @@ struct ReplyThreadOverlay: View {
|
||||
|
||||
Button(action: onDismiss) {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 13, weight: .bold))
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.foregroundStyle(.white.opacity(0.82))
|
||||
.frame(width: 32, height: 32)
|
||||
.background(.white.opacity(0.12), in: Circle())
|
||||
.padding(7)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(.glass)
|
||||
.buttonBorderShape(.circle)
|
||||
.background(.clear)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
.accessibilityLabel("Закрыть ветку ответов")
|
||||
.padding(.top, max(12, geometry.safeAreaInsets.top + 6))
|
||||
|
||||
@@ -54,10 +54,6 @@ private struct SearchField: View {
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 7)
|
||||
.glassEffect()
|
||||
// .background(
|
||||
// Color.secondary.opacity(0.14),
|
||||
// in: RoundedRectangle(cornerRadius: 10)
|
||||
// )
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user