add server info screen, connection stats, and emoji picker
Server information (Monal-style): - Add ServerInformation model and ServerInfoView reachable from Settings, showing server software (XEP-0092), disco#info identities and features, conference (MUC) services via disco#items, and STUN/TURN via XEP-0215. - Add a curated XEP capability list with per-XEP success/error status derived from server/account disco features and connection flags. - Detect PEP (0163) from account disco pubsub#* features, HTTP Upload (0363) from the main domain plus its components, and add Roster Versioning (0237), Pre-Authenticated Roster Subscription (0379), and SASL SCRAM Downgrade Protection (0474, neutral because Martin does not expose SSDP). Connection statistics: - Add LumaConnectionStatsModule, an XmppStanzaFilter registered before stream management, to count sent / acknowledged / received stanzas. - Track last login and SMACKS session timestamps, and read SASL mechanisms and Client State Indication from the raw stream features. MAM / OMEMO handling: - Prefer a plaintext <body> fallback when OMEMO decryption fails instead of showing "failed to decrypt". - Keep the optimistic text of our own outgoing message when its echo cannot be decrypted back, instead of replacing it with a placeholder. Emoji picker: - Add EmojiCatalog and a cross-platform EmojiPickerView. - Open the picker for reactions (replacing the static quick list) and from the composer's smiley button, inserting the chosen emoji into the draft.
This commit is contained in:
1 parent
a886917dc8
commit
0317277d46
12 files changed
+1081
-24
No files matched your search
@@ -9,6 +9,18 @@ import UniformTypeIdentifiers
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
private enum EmojiPickerPresentation: Identifiable {
|
||||
case reaction(ChatMessage)
|
||||
case composer
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .reaction(let message): return "reaction-\(message.id)"
|
||||
case .composer: return "composer"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
struct ChatView: View {
|
||||
private static let bottomAnchorID = "luma-chat-timeline-bottom"
|
||||
@@ -42,6 +54,7 @@ struct ChatView: View {
|
||||
@State private var selectedMessageIDs: Set<String> = []
|
||||
@State private var destructiveAction: MessageDestructiveAction?
|
||||
@State private var replyThreadSelection: ReplyThreadSelection?
|
||||
@State private var emojiPickerPresentation: EmojiPickerPresentation?
|
||||
@State private var hasCompletedInitialScroll = false
|
||||
@State private var isNearTimelineBottom = true
|
||||
@State private var historyLoadAnchorID: String?
|
||||
@@ -223,6 +236,11 @@ struct ChatView: View {
|
||||
clearMessageSelection()
|
||||
}
|
||||
}
|
||||
.sheet(item: $emojiPickerPresentation) { presentation in
|
||||
EmojiPickerView { emoji in
|
||||
handleEmojiSelection(emoji, for: presentation)
|
||||
}
|
||||
}
|
||||
.alert(item: $destructiveAction) { action in
|
||||
Alert(
|
||||
title: Text(action.title),
|
||||
@@ -404,6 +422,9 @@ struct ChatView: View {
|
||||
},
|
||||
onReact: { emoji in
|
||||
Task { await model.toggleReaction(emoji, on: message) }
|
||||
},
|
||||
onReactPicker: {
|
||||
emojiPickerPresentation = .reaction(message)
|
||||
}
|
||||
)
|
||||
.id(message.id)
|
||||
@@ -608,11 +629,16 @@ struct ChatView: View {
|
||||
.submitLabel(.send)
|
||||
.onSubmit { sendDraft() }
|
||||
|
||||
Image(systemName: "face.smiling")
|
||||
.font(.system(size: 20, weight: .regular))
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 1)
|
||||
.accessibilityHidden(true)
|
||||
Button {
|
||||
emojiPickerPresentation = .composer
|
||||
} label: {
|
||||
Image(systemName: "face.smiling")
|
||||
.font(.system(size: 20, weight: .regular))
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 1)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Эмодзи")
|
||||
}
|
||||
.padding(.leading, 14)
|
||||
.padding(.trailing, 12)
|
||||
@@ -782,6 +808,7 @@ struct ChatView: View {
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.16), value: model.typingText(for: liveConversation))
|
||||
.accessibilityElement(children: .combine)
|
||||
.padding()
|
||||
}
|
||||
|
||||
private var canSendToConversation: Bool {
|
||||
@@ -1385,6 +1412,18 @@ struct ChatView: View {
|
||||
isComposerFocused = true
|
||||
}
|
||||
|
||||
private func handleEmojiSelection(
|
||||
_ emoji: String,
|
||||
for presentation: EmojiPickerPresentation
|
||||
) {
|
||||
switch presentation {
|
||||
case .reaction(let message):
|
||||
Task { await model.toggleReaction(emoji, on: message) }
|
||||
case .composer:
|
||||
draft.append(emoji)
|
||||
}
|
||||
}
|
||||
|
||||
private func beginMessageSelection(with message: ChatMessage) {
|
||||
cancelComposerRecording(feedback: false)
|
||||
dismissKeyboard()
|
||||
|
||||
Reference in new issue
Block a user