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

@@ -0,0 +1,92 @@
import SwiftUI
/// A reusable emoji picker: category tabs on the bottom and a grid of emojis
/// for the selected category. Used both for reactions and for inserting an
/// emoji into the composer.
struct EmojiPickerView: View {
let onSelect: (String) -> Void
@Environment(\.dismiss) private var dismiss
@State private var selectedCategoryID = EmojiCatalog.categories.first?.id ?? ""
private let columns = [GridItem(.adaptive(minimum: 44), spacing: 4)]
private var selectedCategory: EmojiCategory {
EmojiCatalog.categories.first { $0.id == selectedCategoryID }
?? EmojiCatalog.categories[0]
}
var body: some View {
VStack(spacing: 0) {
header
ScrollView {
LazyVGrid(columns: columns, spacing: 6) {
ForEach(selectedCategory.emojis, id: \.self) { emoji in
Button {
onSelect(emoji)
dismiss()
} label: {
Text(emoji)
.font(.system(size: 28))
.frame(width: 46, height: 46)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityLabel(emoji)
}
}
.padding(.horizontal, 12)
.padding(.top, 8)
.padding(.bottom, 12)
}
Divider()
categorySelector
.padding(.vertical, 8)
}
.frame(minHeight: 360)
#if os(macOS)
.frame(minWidth: 380)
#endif
}
private var header: some View {
HStack {
Text("Выберите эмодзи")
.font(.headline)
Spacer()
Button("Готово") { dismiss() }
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
private var categorySelector: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 6) {
ForEach(EmojiCatalog.categories) { category in
let isSelected = category.id == selectedCategoryID
Button {
selectedCategoryID = category.id
} label: {
Image(systemName: category.symbol)
.font(.system(size: 18, weight: .medium))
.foregroundStyle(isSelected ? Color.accentColor : Color.secondary)
.frame(width: 44, height: 44)
.background(
isSelected
? Color.accentColor.opacity(0.15)
: Color.clear,
in: RoundedRectangle(cornerRadius: 10, style: .continuous)
)
}
.buttonStyle(.plain)
.accessibilityLabel(category.title)
}
}
.padding(.horizontal, 12)
}
}
}
@@ -23,6 +23,7 @@ struct MessageBubble: View {
let onDelete: () -> Void
let onReplyTap: (String) -> Void
let onReact: (String) -> Void
let onReactPicker: () -> Void
@State private var replySwipeOffset: CGFloat = 0
@State private var replySwipeArmed = false
@@ -56,11 +57,7 @@ struct MessageBubble: View {
.contextMenu {
if !isSelectionMode {
if message.canBeReactedTo {
Menu {
ForEach(MessageReactionPolicy.quickChoices, id: \.self) { emoji in
Button(emoji) { onReact(emoji) }
}
} label: {
Button(action: onReactPicker) {
Label("Реакция", systemImage: "face.smiling")
}
}