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.
93 lines
3.1 KiB
Swift
93 lines
3.1 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|