1// NoteToSelfView.swift
2// SophaxChat
3//
4// A local encrypted notepad. Messages are stored in MessageStore under the
5// synthetic peerID "__note_to_self__" and never transmitted over the network.
6
7import SwiftUI
8import SophaxChatCore
9
10struct NoteToSelfView: View {
11 @EnvironmentObject var appState: AppState
12 @FocusState private var isInputFocused: Bool
13 @State private var noteText: String = ""
14
15 private var notes: [StoredMessage] {
16 appState.noteToSelfMessages
17 }
18
19 var body: some View {
20 VStack(spacing: 0) {
21 noteList
22 Divider()
23 inputBar
24 }
25 .navigationTitle("Note to Self")
26 .navigationBarTitleDisplayMode(.inline)
27 .toolbar {
28 ToolbarItem(placement: .principal) {
29 VStack(spacing: 0) {
30 Text("Note to Self")
31 .font(.headline)
32 Text("Encrypted — local only")
33 .font(.caption2)
34 .foregroundStyle(.secondary)
35 }
36 }
37 }
38 }
39
40 // MARK: - Note list
41
42 private var noteList: some View {
43 ScrollViewReader { proxy in
44 ScrollView {
45 LazyVStack(spacing: 8) {
46 ForEach(notes) { note in
47 noteBubble(for: note)
48 .id(note.id)
49 }
50 Color.clear.frame(height: 1).id("bottom")
51 }
52 .padding(.horizontal, 16)
53 .padding(.top, 8)
54 .padding(.bottom, 12)
55 }
56 .onChange(of: notes.count) { _, _ in
57 withAnimation { proxy.scrollTo("bottom", anchor: .bottom) }
58 }
59 .onAppear {
60 proxy.scrollTo("bottom", anchor: .bottom)
61 }
62 }
63 }
64
65 // MARK: - Note bubble
66
67 @ViewBuilder private func noteBubble(for note: StoredMessage) -> some View {
68 HStack {
69 Spacer(minLength: 60)
70 VStack(alignment: .trailing, spacing: 3) {
71 Text(note.body)
72 .font(.body)
73 .padding(.horizontal, 12)
74 .padding(.vertical, 8)
75 .background(Color.accentColor.opacity(0.85))
76 .foregroundStyle(.white)
77 .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
78 .contextMenu {
79 Button {
80 UIPasteboard.general.string = note.body
81 } label: {
82 Label("Copy", systemImage: "doc.on.doc")
83 }
84 Divider()
85 Button(role: .destructive) {
86 appState.deleteNoteToSelf(note)
87 } label: {
88 Label("Delete", systemImage: "trash")
89 }
90 }
91
92 Text(note.timestamp, style: .time)
93 .font(.caption2)
94 .foregroundStyle(.secondary)
95 }
96 }
97 }
98
99 // MARK: - Input bar
100
101 private var inputBar: some View {
102 HStack(spacing: 10) {
103 TextField("Write a note…", text: $noteText, axis: .vertical)
104 .lineLimit(1...5)
105 .padding(.horizontal, 12)
106 .padding(.vertical, 8)
107 .background(Color(.secondarySystemBackground))
108 .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
109 .focused($isInputFocused)
110
111 Button {
112 let trimmed = noteText.trimmingCharacters(in: .whitespacesAndNewlines)
113 guard !trimmed.isEmpty else { return }
114 appState.sendNoteToSelf(trimmed)
115 noteText = ""
116 } label: {
117 Image(systemName: "arrow.up.circle.fill")
118 .font(.system(size: 32))
119 .foregroundStyle(noteText.trimmingCharacters(in: .whitespaces).isEmpty ? Color.secondary : Color.accentColor)
120 }
121 .disabled(noteText.trimmingCharacters(in: .whitespaces).isEmpty)
122 }
123 .padding(.horizontal, 14)
124 .padding(.vertical, 10)
125 }
126}