add server info screen, connection stats, and emoji picker
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s

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:
wt committed 2026-08-18 21:44:27 +07:00
1 parent a886917dc8
commit 0317277d46
12 files changed
+1081 -24

No files matched your search

+44 -5
View File
@@ -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()