Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to new SwiftUI navigation types and make ChatChannel non-optional #31

Merged
merged 6 commits into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions DistributedChat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
C7734DA625BE37310057DF96 /* Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7734DA525BE37310057DF96 /* Navigation.swift */; };
C77C1DE025BBB127005D0B7C /* ViewUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77C1DDF25BBB127005D0B7C /* ViewUtils.swift */; };
C77C1DE825BBC465005D0B7C /* AutoFocusTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77C1DE725BBC465005D0B7C /* AutoFocusTextField.swift */; };
C77C1DF625BBC815005D0B7C /* ProfileVIew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77C1DF525BBC815005D0B7C /* ProfileVIew.swift */; };
C77C1DF625BBC815005D0B7C /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77C1DF525BBC815005D0B7C /* ProfileView.swift */; };
C77D498725BC73F70073A964 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77D498625BC73F70073A964 /* SettingsView.swift */; };
C77D499525BC75B40073A964 /* MessageHistoryStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77D499425BC75B40073A964 /* MessageHistoryStyle.swift */; };
C77D499D25BC76010073A964 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77D499C25BC76010073A964 /* Settings.swift */; };
Expand Down Expand Up @@ -110,7 +110,7 @@
C7734DA525BE37310057DF96 /* Navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigation.swift; sourceTree = "<group>"; };
C77C1DDF25BBB127005D0B7C /* ViewUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewUtils.swift; sourceTree = "<group>"; };
C77C1DE725BBC465005D0B7C /* AutoFocusTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoFocusTextField.swift; sourceTree = "<group>"; };
C77C1DF525BBC815005D0B7C /* ProfileVIew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileVIew.swift; sourceTree = "<group>"; };
C77C1DF525BBC815005D0B7C /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
C77D498625BC73F70073A964 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
C77D499425BC75B40073A964 /* MessageHistoryStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageHistoryStyle.swift; sourceTree = "<group>"; };
C77D499C25BC76010073A964 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -240,7 +240,7 @@
C7C24EEF25C8DD6900222AAA /* QuickLookAttachmentView.swift */,
C77D49D625BC7B600073A964 /* NetworkView.swift */,
C77DBACC25CCC92D00D66EB5 /* PresenceView.swift */,
C77C1DF525BBC815005D0B7C /* ProfileVIew.swift */,
C77C1DF525BBC815005D0B7C /* ProfileView.swift */,
C77D498625BC73F70073A964 /* SettingsView.swift */,
C77C1DDF25BBB127005D0B7C /* ViewUtils.swift */,
C77C1DE725BBC465005D0B7C /* AutoFocusTextField.swift */,
Expand Down Expand Up @@ -487,7 +487,7 @@
C7734DA625BE37310057DF96 /* Navigation.swift in Sources */,
C7982D2225BB8885000DC0F1 /* ChannelsView.swift in Sources */,
C7734D1225BD00330057DF96 /* QuickLookAttachment.swift in Sources */,
C77C1DF625BBC815005D0B7C /* ProfileVIew.swift in Sources */,
C77C1DF625BBC815005D0B7C /* ProfileView.swift in Sources */,
C7982D4125BB8A57000DC0F1 /* Messages.swift in Sources */,
C7ED97B825C5F4BA002ACC5A /* PersistenceError.swift in Sources */,
C7C24EF025C8DD6A00222AAA /* QuickLookAttachmentView.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion DistributedChatApp/DistributedChatApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ struct DistributedChatApp: App {
}
} else {
log.debug("Parsed URL as #global...")
state.navigation.open(channel: nil)
state.navigation.open(channel: .global)
}
case ["/", "message"]:
if components.count == 3, let id = UUID(uuidString: components[2]), let message = state.messages[id] {
Expand Down
18 changes: 9 additions & 9 deletions DistributedChatApp/Model/Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ import Logging
fileprivate let log = Logger(label: "DistributedChatApp.Messages")

class Messages: ObservableObject {
@Published var autoReadChannels: Set<ChatChannel?> = []
@Published var autoReadChannels: Set<ChatChannel> = []
@Published(persistingTo: "Messages/unreadMessageIds.json") var unreadMessageIds: Set<UUID> = []
@Published(persistingTo: "Messages/pinnedChannels.json") private(set) var pinnedChannels: Set<ChatChannel?> = [nil]
@Published(persistingTo: "Messages/pinnedChannels.json") private(set) var pinnedChannels: Set<ChatChannel> = [.global]
@Published(persistingTo: "Messages/messages.json") private(set) var messages: [UUID: ChatMessage] = [:]

var unreadChannels: Set<ChatChannel?> { Set(unreadMessageIds.compactMap { messages[$0] }.map(\.channel)) }
var channels: [ChatChannel?] {
pinnedChannels.sorted { ($0.map { "\($0)" } ?? "") < ($1.map { "\($0)" } ?? "") } + messages.values
var unreadChannels: Set<ChatChannel> { Set(unreadMessageIds.compactMap { messages[$0] }.map(\.channel)) }
var channels: [ChatChannel] {
pinnedChannels.sorted { String(describing: $0) < String(describing: $1) } + messages.values
.sorted { $0.timestamp > $1.timestamp }
.compactMap(\.channel)
.map(\.channel)
.filter { !pinnedChannels.contains($0) }
.distinct
}
Expand Down Expand Up @@ -97,12 +97,12 @@ class Messages: ObservableObject {
unreadMessageIds = unreadMessageIds.filter { messages[$0]?.channel != channel }
}

func pin(channel: ChatChannel?) {
func pin(channel: ChatChannel) {
pinnedChannels.insert(channel)
}

func unpin(channel: ChatChannel?) {
if channel != nil { // #global cannot be unpinned
func unpin(channel: ChatChannel) {
if channel != .global { // #global cannot be unpinned
pinnedChannels.remove(channel)
}
}
Expand Down
2 changes: 1 addition & 1 deletion DistributedChatApp/View/ChannelSnippetView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import SwiftUI
import DistributedChatKit

struct ChannelSnippetView: View {
let channel: ChatChannel?
let channel: ChatChannel

@EnvironmentObject private var messages: Messages
@EnvironmentObject private var settings: Settings
Expand Down
4 changes: 2 additions & 2 deletions DistributedChatApp/View/ChannelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import DistributedChatKit
import SwiftUI

struct ChannelView: View {
let channel: ChatChannel?
let channel: ChatChannel
let controller: ChatController

@EnvironmentObject private var messages: Messages
Expand Down Expand Up @@ -47,7 +47,7 @@ struct ChatView_Previews: PreviewProvider {
@StateObject static var network = Network(myId: controller.me.id, messages: messages)
@StateObject static var navigation = Navigation()
static var previews: some View {
ChannelView(channel: nil, controller: controller)
ChannelView(channel: .global, controller: controller)
.environmentObject(messages)
.environmentObject(settings)
.environmentObject(network)
Expand Down
35 changes: 19 additions & 16 deletions DistributedChatApp/View/ChannelsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import DistributedChatKit
import SwiftUI

struct ChannelsView: View {
let channels: [ChatChannel?]
let channels: [ChatChannel]
let controller: ChatController

@EnvironmentObject private var messages: Messages
Expand All @@ -18,24 +18,24 @@ struct ChannelsView: View {
@EnvironmentObject private var network: Network
@State private var newChannels: [ChatChannel] = []
@State private var channelDraftSheetShown: Bool = false
@State private var deletingChannels: [ChatChannel?] = []
@State private var deletingChannels: [ChatChannel] = []
@State private var deletionConfirmationShown: Bool = false

private var allChannels: [ChatChannel?] {
private var allChannels: [ChatChannel] {
channels + newChannels.filter { !channels.contains($0) }
}

var body: some View {
NavigationView {
List {
NavigationSplitView {
List(selection: $navigation.activeChannel) {
let nearbyCount = network.nearbyUsers.count
let reachableCount = network.presences.filter { $0.key != controller.me.id }.count
HStack {
Image(systemName: "antenna.radiowaves.left.and.right")
Text("\(reachableCount) \("user".pluralized(with: reachableCount)) reachable, \(nearbyCount) \("user".pluralized(with: nearbyCount)) nearby")
}
ForEach(allChannels, id: \.self) { channel in
NavigationLink(destination: ChannelView(channel: channel, controller: controller), tag: channel, selection: $navigation.activeChannel) {
NavigationLink(value: channel) {
ChannelSnippetView(channel: channel)
}
.contextMenu {
Expand All @@ -61,24 +61,22 @@ struct ChannelsView: View {
Text("Pin")
Image(systemName: "pin.fill")
}
} else if channel != nil {
} else if channel != .global {
Button(action: {
messages.unpin(channel: channel)
}) {
Text("Unpin")
Image(systemName: "pin.slash.fill")
}
}
if let channel = channel {
Button(action: {
UIPasteboard.general.string = channel.displayName(with: network)
}) {
Text("Copy Channel Name")
Image(systemName: "doc.on.doc")
}
Button(action: {
UIPasteboard.general.string = channel.displayName(with: network)
}) {
Text("Copy Channel Name")
Image(systemName: "doc.on.doc")
}
Button(action: {
UIPasteboard.general.url = URL(string: "distributedchat:///channel\(channel.map { "/\($0)" } ?? "")")
UIPasteboard.general.url = URL(string: "distributedchat:///channel/\(channel)")
}) {
Text("Copy Channel URL")
Image(systemName: "doc.on.doc.fill")
Expand All @@ -102,8 +100,13 @@ struct ChannelsView: View {
}
}
}
} detail: {
Group {
if let channel = navigation.activeChannel {
ChannelView(channel: channel, controller: controller)
}
}
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
.sheet(isPresented: $channelDraftSheetShown) {
NewChannelView {
channelDraftSheetShown = false
Expand Down
4 changes: 2 additions & 2 deletions DistributedChatApp/View/MessageComposeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ fileprivate let log = Logger(label: "DistributedChatApp.MessageComposeView")
fileprivate let compression: ChatAttachment.Compression = .lzfse

struct MessageComposeView: View {
let channel: ChatChannel?
let channel: ChatChannel
let controller: ChatController
@Binding var replyingToMessageId: UUID?

Expand Down Expand Up @@ -217,7 +217,7 @@ struct MessageComposeView_Previews: PreviewProvider {
@StateObject static var network = Network(messages: messages)
@State static var replyingToMessageId: UUID? = nil
static var previews: some View {
MessageComposeView(channel: nil, controller: controller, replyingToMessageId: $replyingToMessageId)
MessageComposeView(channel: .global, controller: controller, replyingToMessageId: $replyingToMessageId)
.environmentObject(messages)
.environmentObject(network)
}
Expand Down
3 changes: 1 addition & 2 deletions DistributedChatApp/View/NetworkView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ struct NetworkView: View {
@EnvironmentObject private var navigation: Navigation

var body: some View {
NavigationView {
NavigationStack {
Form {
Section(header: Text("Nearby Users")) {
List(network.nearbyUsers) { user in
Expand Down Expand Up @@ -81,7 +81,6 @@ struct NetworkView: View {
}
.navigationTitle("Network")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ struct ProfileView: View {
@EnvironmentObject private var profile: Profile

var body: some View {
NavigationView {
NavigationStack {
VStack(alignment: .center, spacing: 40) {
EnumPicker(selection: $profile.presence.status, label: ZStack(alignment: .bottomTrailing) {
Image(systemName: "person.circle.fill")
Expand All @@ -35,7 +35,6 @@ struct ProfileView: View {
.padding(20)
.navigationTitle("Profile")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}

Expand Down
3 changes: 1 addition & 2 deletions DistributedChatApp/View/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ struct SettingsView: View {
@EnvironmentObject private var settings: Settings

var body: some View {
NavigationView {
NavigationStack {
Form {
Section(header: Text("Presentation")) {
EnumPicker(selection: $settings.presentation.messageHistoryStyle, label: Text("Message History Style"))
Expand Down Expand Up @@ -51,7 +51,6 @@ struct SettingsView: View {
}
.navigationTitle("Settings")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}

Expand Down
18 changes: 4 additions & 14 deletions DistributedChatApp/View/ViewUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ let iconSize: CGFloat = 22
/// The displayed name of the 'global' channel, internally represented with nil
fileprivate let globalChannelName = "global"

extension Optional where Wrapped == ChatChannel {
extension ChatChannel {
func rawDisplayName(with network: Network) -> String {
switch self {
case .room(let name)?:
case .room(let name):
return name
case .dm(let userIds)?:
case .dm(let userIds):
if userIds.count == 1, let userId = userIds.first {
return name(of: userId, with: network)
} else {
Expand All @@ -28,7 +28,7 @@ extension Optional where Wrapped == ChatChannel {
.map { name(of: $0, with: network) }
.joined(separator: ",")
}
case nil:
case .global:
return globalChannelName
}
}
Expand All @@ -46,13 +46,3 @@ extension Optional where Wrapped == ChatChannel {
}
}
}

extension ChatChannel {
func rawDisplayName(with network: Network) -> String {
Optional.some(self).rawDisplayName(with: network)
}

func displayName(with network: Network) -> String {
Optional.some(self).displayName(with: network)
}
}
6 changes: 3 additions & 3 deletions DistributedChatApp/ViewModel/Navigation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import Combine
import DistributedChatKit

class Navigation: ObservableObject {
@Published var activeChannel: ChatChannel?? = nil
@Published var activeChannel: ChatChannel? = nil

func open(channel: ChatChannel?) {
activeChannel = Optional.some(channel)
func open(channel: ChatChannel) {
activeChannel = channel
}
}
16 changes: 8 additions & 8 deletions DistributedChatCLI/Sources/DistributedChatCLI/ChatREPL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,17 @@ class ChatREPL {
]
}

private func displayName(of channel: ChatChannel?) -> String {
private func displayName(of channel: ChatChannel) -> String {
switch channel {
case .dm(let userIds)?:
case .dm(let userIds):
let name = userIds
.filter { $0 != controller.me.id }
.map { network.presences[$0]?.user.displayName ?? $0.uuidString }
.joined(separator: ",")
return "@\(name)"
case .room(let name)?:
case .room(let name):
return "#\(name)"
case nil:
case .global:
return "#\(globalChannelName)"
}
}
Expand All @@ -73,19 +73,19 @@ class ChatREPL {
case "@"?:
return resolveUser(from: name).map { .dm([controller.me.id, $0]) }
case "#"?:
return name == globalChannelName ? nil : .room(name)
return name == globalChannelName ? .global : .room(name)
default:
return nil
}
}

private func parseMessage(from raw: String) -> (String, ChatChannel?) {
private func parseMessage(from raw: String) -> (String, ChatChannel) {
let split = raw.split(separator: " ", maxSplits: 1).map(String.init)

if split.count == 2, let channel = parseChannel(from: split[0]) {
return (split[1], channel)
} else {
return (raw, nil) // on #global
return (raw, .global)
}
}

Expand Down Expand Up @@ -127,7 +127,7 @@ class ChatREPL {
command()
} else {
let (content, channel) = parseMessage(from: input)
controller.send(content: content, on: channel)
controller.send(content: content, on: channel ?? .global)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public class ChatController {
}
}

public func send(content: String, on channel: ChatChannel? = nil, attaching attachments: [ChatAttachment]? = nil, replyingTo repliedToMessageId: UUID? = nil) {
public func send(content: String, on channel: ChatChannel = .global, attaching attachments: [ChatAttachment]? = nil, replyingTo repliedToMessageId: UUID? = nil) {
let chatMessage = ChatMessage(
author: me,
content: .text(content),
Expand Down
Loading