Note Sharing, distributing updates
This commit is contained in:
@@ -12,14 +12,16 @@ extension EnvironmentValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@AppStorage("peered_username") private var username: String = ""
|
@AppStorage("peered_username") private var username: String = "fallback_user"
|
||||||
@State private var notes = [Note]()
|
@State private var notes = [Note]()
|
||||||
@State private var notesClient: NoteEditingSessionClient?
|
@State private var notesClient: NoteEditingSessionClient?
|
||||||
@State private var ownPeer: OwnPeer?
|
@State private var ownPeer: OwnPeer?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
List(notes) { note in
|
List {
|
||||||
|
Section("Your notes") {
|
||||||
|
ForEach(notes) { note in
|
||||||
NavigationLink(note.name) {
|
NavigationLink(note.name) {
|
||||||
let peer = ownPeer ?? .init(peer: .init(displayName: username))
|
let peer = ownPeer ?? .init(peer: .init(displayName: username))
|
||||||
if ownPeer == nil {
|
if ownPeer == nil {
|
||||||
@@ -28,6 +30,18 @@ struct ContentView: View {
|
|||||||
return NoteEditorScreen(note: note, peer: peer)
|
return NoteEditorScreen(note: note, peer: peer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let notesClient {
|
||||||
|
Section("External notes") {
|
||||||
|
ForEach(notesClient.invitations) { invitation in
|
||||||
|
NavigationLink(invitation.noteName) {
|
||||||
|
SharedNoteEditor(invitation: invitation, noteClient: notesClient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.environment(\.ownPeer, ownPeer ?? .fallback)
|
.environment(\.ownPeer, ownPeer ?? .fallback)
|
||||||
.navigationTitle("Peered")
|
.navigationTitle("Peered")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
@@ -44,7 +58,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
notesClient?.startBrowsingForNotes()
|
notesClient?.startBrowsingForNotes()
|
||||||
}
|
}
|
||||||
.sheet(isPresented: .constant(username.isEmpty)) {
|
.sheet(isPresented: .constant(username == "fallback_user" || username.isEmpty)) {
|
||||||
SetUserNameBottomSheetView(username: $username)
|
SetUserNameBottomSheetView(username: $username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import SwiftUI
|
|||||||
|
|
||||||
struct ManageMembersView: View {
|
struct ManageMembersView: View {
|
||||||
@Bindable var noteAdvertiser: NoteEditingSessionServer
|
@Bindable var noteAdvertiser: NoteEditingSessionServer
|
||||||
|
let noteTitle: String
|
||||||
|
@Binding var noteContent: String
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List(noteAdvertiser.visiblePeers) { peer in
|
List(noteAdvertiser.visiblePeers) { peer in
|
||||||
@@ -17,7 +19,7 @@ struct ManageMembersView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
PeerStateButton(peerState: peer.state) {
|
PeerStateButton(peerState: peer.state) {
|
||||||
noteAdvertiser.invite(peer: peer)
|
noteAdvertiser.invite(peer: peer, to: .init(title: noteTitle, noteSnapshot: noteContent))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import MultipeerConnectivity
|
import MultipeerConnectivity
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
struct OwnPeer {
|
struct OwnPeer {
|
||||||
let peer: MCPeerID
|
let peer: MCPeerID
|
||||||
@@ -29,6 +30,7 @@ final class NoteEditingSessionServer: NSObject {
|
|||||||
private let ownPeer: OwnPeer
|
private let ownPeer: OwnPeer
|
||||||
|
|
||||||
var visiblePeers: [Peer] = []
|
var visiblePeers: [Peer] = []
|
||||||
|
let noteChangesEmitter = PassthroughSubject<String, Never>()
|
||||||
|
|
||||||
init(peer: OwnPeer) {
|
init(peer: OwnPeer) {
|
||||||
ownPeer = peer
|
ownPeer = peer
|
||||||
@@ -36,6 +38,7 @@ final class NoteEditingSessionServer: NSObject {
|
|||||||
session = .init(peer: peer.peer)
|
session = .init(peer: peer.peer)
|
||||||
super.init()
|
super.init()
|
||||||
browser.delegate = self
|
browser.delegate = self
|
||||||
|
session.delegate = self
|
||||||
}
|
}
|
||||||
|
|
||||||
func startServer() {
|
func startServer() {
|
||||||
@@ -46,14 +49,17 @@ final class NoteEditingSessionServer: NSObject {
|
|||||||
browser.stopBrowsingForPeers()
|
browser.stopBrowsingForPeers()
|
||||||
}
|
}
|
||||||
|
|
||||||
func invite(peer: Peer) {
|
func invite(peer: Peer, to note: NoteInvitation.NoteContent) {
|
||||||
guard peer.state == .available else { return }
|
guard peer.state == .available else { return }
|
||||||
browser.invitePeer(
|
browser.invitePeer(
|
||||||
peer.mcPeer,
|
peer.mcPeer,
|
||||||
to: session,
|
to: session,
|
||||||
withContext: nil, // FIXME: put note here?
|
withContext: try! JSONEncoder().encode(note),
|
||||||
timeout: 5
|
timeout: 5
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let idxToUpdate = visiblePeers.firstIndex(where: { $0.mcPeer == peer.mcPeer })!
|
||||||
|
visiblePeers[idxToUpdate].state = .invitationPending
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,3 +79,46 @@ extension NoteEditingSessionServer: MCNearbyServiceBrowserDelegate {
|
|||||||
visiblePeers.remove(at: peerIdx)
|
visiblePeers.remove(at: peerIdx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NoteEditingSessionServer: MCSessionDelegate {
|
||||||
|
func session(
|
||||||
|
_ session: MCSession,
|
||||||
|
peer peerID: MCPeerID,
|
||||||
|
didChange state: MCSessionState
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func session(
|
||||||
|
_ session: MCSession,
|
||||||
|
didReceive stream: InputStream,
|
||||||
|
withName streamName: String,
|
||||||
|
fromPeer peerID: MCPeerID
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func session(
|
||||||
|
_ session: MCSession,
|
||||||
|
didStartReceivingResourceWithName resourceName: String,
|
||||||
|
fromPeer peerID: MCPeerID,
|
||||||
|
with progress: Progress
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func session(
|
||||||
|
_ session: MCSession,
|
||||||
|
didFinishReceivingResourceWithName resourceName: String,
|
||||||
|
fromPeer peerID: MCPeerID,
|
||||||
|
at localURL: URL?,
|
||||||
|
withError error: (any Error)?
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
|
||||||
|
guard let note = String(data: data, encoding: .utf8) else { fatalError() }
|
||||||
|
noteChangesEmitter.send(note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,9 +32,16 @@ struct NoteEditorScreen: View {
|
|||||||
}
|
}
|
||||||
.sheet(isPresented: $showManageMembers) {
|
.sheet(isPresented: $showManageMembers) {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
ManageMembersView(noteAdvertiser: noteAdvertiser)
|
ManageMembersView(
|
||||||
|
noteAdvertiser: noteAdvertiser,
|
||||||
|
noteTitle: note.name,
|
||||||
|
noteContent: $noteContent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onReceive(noteAdvertiser.noteChangesEmitter) { updatedNote in
|
||||||
|
self.noteContent = updatedNote
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
noteContent = try! String(contentsOf: note.path, encoding: .utf8)
|
noteContent = try! String(contentsOf: note.path, encoding: .utf8)
|
||||||
noteAdvertiser.startServer()
|
noteAdvertiser.startServer()
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
//
|
||||||
|
// SharedNoteEditor.swift
|
||||||
|
// Peered
|
||||||
|
//
|
||||||
|
// Created by Oskar Chybowski on 07/10/2025.
|
||||||
|
//
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SharedNoteEditor: View {
|
||||||
|
@State var note: String?
|
||||||
|
@State var sendTask: Task<Void, Never>?
|
||||||
|
@State var invitation: NoteInvitation
|
||||||
|
@Bindable var noteClient: NoteEditingSessionClient
|
||||||
|
|
||||||
|
init(
|
||||||
|
invitation: NoteInvitation,
|
||||||
|
noteClient: NoteEditingSessionClient
|
||||||
|
) {
|
||||||
|
self._invitation = .init(initialValue: invitation)
|
||||||
|
self._noteClient = .init(noteClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
if let note = Binding($note) {
|
||||||
|
TextEditor(text: note)
|
||||||
|
} else {
|
||||||
|
ProgressView {
|
||||||
|
Text("Fetching note...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: note) { _, newValue in
|
||||||
|
sendTask?.cancel()
|
||||||
|
sendTask = Task {
|
||||||
|
try? await Task.sleep(nanoseconds: 500000000)
|
||||||
|
guard let note else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
noteClient.send(note: note, to: invitation.invitatorID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
invitation.accept()
|
||||||
|
note = invitation.note.noteSnapshot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,53 @@
|
|||||||
//
|
|
||||||
// NoteEditingSessionClient.swift
|
|
||||||
// Peered
|
|
||||||
//
|
|
||||||
// Created by Oskar Chybowski on 05/10/2025.
|
|
||||||
//
|
|
||||||
|
|
||||||
|
|
||||||
import MultipeerConnectivity
|
import MultipeerConnectivity
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
struct NoteInvitation: Identifiable {
|
||||||
|
struct NoteContent: Codable {
|
||||||
|
let title: String
|
||||||
|
let noteSnapshot: String
|
||||||
|
}
|
||||||
|
|
||||||
|
var id: MCPeerID { invitatorID }
|
||||||
|
let noteName: String
|
||||||
|
let invitatorID: MCPeerID
|
||||||
|
let note: NoteContent
|
||||||
|
private var invitationHandler: ((Bool) -> Void)?
|
||||||
|
|
||||||
|
init(
|
||||||
|
noteName: String,
|
||||||
|
invitatorID: MCPeerID,
|
||||||
|
note: NoteContent,
|
||||||
|
invitationHandler: ((Bool) -> Void)? = nil
|
||||||
|
) {
|
||||||
|
self.noteName = noteName
|
||||||
|
self.invitatorID = invitatorID
|
||||||
|
self.note = note
|
||||||
|
self.invitationHandler = invitationHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func accept() {
|
||||||
|
invitationHandler?(true)
|
||||||
|
invitationHandler = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decline() {
|
||||||
|
invitationHandler?(false)
|
||||||
|
invitationHandler = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Observable
|
@Observable
|
||||||
final class NoteEditingSessionClient: NSObject {
|
final class NoteEditingSessionClient: NSObject {
|
||||||
private let session: MCSession
|
private let session: MCSession
|
||||||
private let advertiser: MCNearbyServiceAdvertiser
|
private let advertiser: MCNearbyServiceAdvertiser
|
||||||
private let ownPeer: MCPeerID
|
private let ownPeer: MCPeerID
|
||||||
|
|
||||||
|
var invitations: [NoteInvitation] = [] {
|
||||||
|
didSet {
|
||||||
|
print(invitations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init(peer: MCPeerID) {
|
init(peer: MCPeerID) {
|
||||||
ownPeer = peer
|
ownPeer = peer
|
||||||
session = MCSession(
|
session = MCSession(
|
||||||
@@ -38,6 +71,14 @@ final class NoteEditingSessionClient: NSObject {
|
|||||||
func stopBrowsingForNotes() {
|
func stopBrowsingForNotes() {
|
||||||
advertiser.stopAdvertisingPeer()
|
advertiser.stopAdvertisingPeer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func send(note: String, to peer: MCPeerID) {
|
||||||
|
try! session.send(
|
||||||
|
note.data(using: .utf8)!,
|
||||||
|
toPeers: [peer],
|
||||||
|
with: .reliable
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NoteEditingSessionClient: MCNearbyServiceAdvertiserDelegate {
|
extension NoteEditingSessionClient: MCNearbyServiceAdvertiserDelegate {
|
||||||
@@ -47,8 +88,26 @@ extension NoteEditingSessionClient: MCNearbyServiceAdvertiserDelegate {
|
|||||||
withContext context: Data?,
|
withContext context: Data?,
|
||||||
invitationHandler: @escaping (Bool, MCSession?) -> Void
|
invitationHandler: @escaping (Bool, MCSession?) -> Void
|
||||||
) {
|
) {
|
||||||
|
guard
|
||||||
|
let context,
|
||||||
|
let noteContent = try? JSONDecoder().decode(NoteInvitation.NoteContent.self, from: context)
|
||||||
|
else { fatalError() }
|
||||||
|
|
||||||
|
invitations.append(
|
||||||
|
.init(
|
||||||
|
noteName: noteContent.title,
|
||||||
|
invitatorID: peerID,
|
||||||
|
note: noteContent,
|
||||||
|
invitationHandler: { [weak self, invitationHandler] accepted in
|
||||||
|
guard let self else { return }
|
||||||
|
invitationHandler(accepted, self.session)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
invitationHandler(true, self.session)
|
guard let idx = self.invitations.firstIndex(where: { $0.id == peerID }) else { return }
|
||||||
|
self.invitations.remove(at: idx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct NoteInvitationView: View {
|
||||||
|
@Binding var invitation: NoteInvitation
|
||||||
|
let joinTapped: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Text(invitation.noteName)
|
||||||
|
Spacer()
|
||||||
|
Button("Join", action: joinTapped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user