diff --git a/Swiftcord/Utils/Extensions/MessagesView+.swift b/Swiftcord/Utils/Extensions/MessagesView+.swift index a1ee5f37..4f602bb7 100644 --- a/Swiftcord/Utils/Extensions/MessagesView+.swift +++ b/Swiftcord/Utils/Extensions/MessagesView+.swift @@ -74,7 +74,7 @@ internal extension MessagesView { Task { do { - _ = try await restAPI.createChannelMsg( + let newMessage = try await restAPI.createChannelMsg( message: NewMessage( content: message, allowed_mentions: allowedMentions, @@ -90,6 +90,8 @@ internal extension MessagesView { attachments: attachments, id: ctx.channel!.id ) + + _ = try await restAPI.ackMessageRead(id: newMessage.channel_id, msgID: newMessage.id) } catch { viewModel.showingInfoBar = true viewModel.infoBarData = InfoBarData( diff --git a/Swiftcord/Views/ContentView.swift b/Swiftcord/Views/ContentView.swift index f4f432a4..8853f316 100644 --- a/Swiftcord/Views/ContentView.swift +++ b/Swiftcord/Views/ContentView.swift @@ -109,6 +109,7 @@ struct ContentView: View { case .guild(let guild): ServerButton( selected: state.selectedGuildID == guild.id || loadingGuildID == guild.id, + guild: guild, name: guild.properties.name, serverIconURL: guild.properties.iconURL(), isLoading: loadingGuildID == guild.id diff --git a/Swiftcord/Views/Message/MessageRenderViews/MessageView.swift b/Swiftcord/Views/Message/MessageRenderViews/MessageView.swift index a452c7e7..fc3f7591 100644 --- a/Swiftcord/Views/Message/MessageRenderViews/MessageView.swift +++ b/Swiftcord/Views/Message/MessageRenderViews/MessageView.swift @@ -41,6 +41,7 @@ struct MessageView: View, Equatable { } let message: Message + let prevMessage: Message? let shrunk: Bool let quotedMsg: Message? let onQuoteClick: (Snowflake) -> Void @@ -159,6 +160,12 @@ struct MessageView: View, Equatable { Text("Edit") } } + + Button(action: { Task { await readMessage() } }) { + Image(systemName: "message.badge") + Text("Mark as unread") + } + Button(role: .destructive, action: deleteMessage) { Image(systemName: "xmark.bin.fill") Text("Delete Message").foregroundColor(.red) @@ -209,6 +216,20 @@ private extension MessageView { func editMessage() { print(#function) } + + func readMessage() async { + do { + let id = Int(floor((message.id as NSString).doubleValue / pow(2, 22)) - 1) + var defaultId: Snowflake + if id < 0 { + defaultId = "0" + } else { + defaultId = String(id << 22) + } + + let _ = try await restAPI.ackMessageRead(id: message.channel_id, msgID: prevMessage?.id ?? defaultId, manual: true, mention_count: 0) + } catch {} + } func deleteMessage() { Task { diff --git a/Swiftcord/Views/Message/MessagesView.swift b/Swiftcord/Views/Message/MessagesView.swift index 13c2b7ad..c4c07679 100644 --- a/Swiftcord/Views/Message/MessagesView.swift +++ b/Swiftcord/Views/Message/MessagesView.swift @@ -159,6 +159,7 @@ struct MessagesView: View { func cell(for msg: Message, shrunk: Bool) -> some View { MessageView( message: msg, + prevMessage: viewModel.messages.after(msg), shrunk: shrunk, quotedMsg: msg.message_reference != nil ? viewModel.messages.first { diff --git a/Swiftcord/Views/Server/ChannelList.swift b/Swiftcord/Views/Server/ChannelList.swift index c0ec7606..51211304 100644 --- a/Swiftcord/Views/Server/ChannelList.swift +++ b/Swiftcord/Views/Server/ChannelList.swift @@ -29,6 +29,26 @@ struct ChannelList: View, Equatable { Circle().fill(.primary).frame(width: 8, height: 8).offset(x: 2) } }) + .contextMenu { + let isRead = gateway.readState[channel.id]?.id == channel.last_message_id + Button(action: { Task { await readChannel(channel) } }) { + Image(systemName: isRead ? "message" : "message.badge") + Text("Mark as read") + }.disabled(isRead) + + Divider() + + Group { + Button(action: { copyLink(channel) }) { + Image(systemName: "link") + Text("Copy Link") + } + Button(action: { copyId(channel) }) { + Image(systemName: "number.circle.fill") + Text("Copy ID") + } + } + } } var body: some View { @@ -77,6 +97,19 @@ struct ChannelList: View, Equatable { Section(header: Text(channel.name ?? "").textCase(.uppercase).padding(.leading, 8)) { ForEach(channels, id: \.id) { channel in item(for: channel) } } + .contextMenu { + Button(action: { Task { await readChannels(channels) } }) { + Image(systemName: "message.badge") + Text("Mark as read") + } + + Divider() + + Button(action: { copyId(channel) }) { + Image(systemName: "number.circle.fill") + Text("Copy ID") + } + } } } } @@ -96,3 +129,35 @@ struct ChannelList: View, Equatable { lhs.channels == rhs.channels && lhs.selCh == rhs.selCh } } + +private extension ChannelList { + func readChannels(_ channels: [Channel]) async { + for channel in channels { + await readChannel(channel) + } + } + + func readChannel(_ channel: Channel) async { + do { + let _ = try await restAPI.ackMessageRead(id: channel.id, msgID: channel.last_message_id ?? "", manual: true, mention_count: 0) + } catch {} + } + + func copyLink(_ channel: Channel) { + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString( + "https://canary.discord.com/channels/\(channel.guild_id ?? "@me")/\(channel.id)", + forType: .string + ) + } + + func copyId(_ channel: Channel) { + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString( + channel.id, + forType: .string + ) + } +} diff --git a/Swiftcord/Views/Server/ServerButton.swift b/Swiftcord/Views/Server/ServerButton.swift index ebe346f1..71c21ec3 100644 --- a/Swiftcord/Views/Server/ServerButton.swift +++ b/Swiftcord/Views/Server/ServerButton.swift @@ -6,6 +6,8 @@ // import SwiftUI +import DiscordKit +import DiscordKitCore import CachedAsyncImage /* @@ -26,6 +28,7 @@ import CachedAsyncImage struct ServerButton: View { let selected: Bool + var guild: PreloadedGuild? let name: String var systemIconName: String? var assetIconName: String? @@ -51,6 +54,7 @@ struct ServerButton: View { .buttonStyle( ServerButtonStyle( selected: selected, + guild: guild, name: name, bgColor: bgColor, systemName: systemIconName, @@ -60,8 +64,15 @@ struct ServerButton: View { hovered: $hovered ) ) - .padding(.trailing, 8) - + .popover(isPresented: $hovered) { + Text(name) + .font(.title3) + .padding(8) + .frame(maxWidth: 300) + .interactiveDismissDisabled() + } + .padding(.trailing, 8) + Spacer() } .frame(width: 72, height: 48) @@ -69,14 +80,17 @@ struct ServerButton: View { } struct ServerButtonStyle: ButtonStyle { - let selected: Bool - let name: String - let bgColor: Color? - let systemName: String? - let assetName: String? - let serverIconURL: String? - let loading: Bool - @Binding var hovered: Bool + let selected: Bool + var guild: PreloadedGuild? + let name: String + let bgColor: Color? + let systemName: String? + let assetName: String? + let serverIconURL: String? + let loading: Bool + @Binding var hovered: Bool + + @EnvironmentObject var gateway: DiscordGateway func makeBody(configuration: Configuration) -> some View { ZStack { @@ -129,20 +143,66 @@ struct ServerButtonStyle: ButtonStyle { } .offset(y: configuration.isPressed ? 1 : 0) .animation(.none, value: configuration.isPressed) - .animation(.interpolatingSpring(stiffness: 500, damping: 30), value: hovered) - .onHover { hover in hovered = hover } + .animation(.interpolatingSpring(stiffness: 500, damping: 30), value: hovered) + .onHover { hover in hovered = hover } + .contextMenu { + if guild != nil { + Text(name) + + Divider() + + Button(action: { Task { await readAll() } }) { + Image(systemName: "message.badge") + Text("Mark as read") + } + + Divider() + + Group { + Button(action: copyLink) { + Image(systemName: "link") + Text("Copy Link") + } + Button(action: copyId) { + Image(systemName: "number.circle.fill") + Text("Copy ID") + } + } + } + } } } -struct ServerButton_Previews: PreviewProvider { - static var previews: some View { - ServerButton( - selected: false, - name: "Hello world, discord!", - systemIconName: nil, - assetIconName: nil, - serverIconURL: nil, - bgColor: nil - ) {} - } +private extension ServerButtonStyle { + func readAll() async { + if let guild = guild { + for channel in guild.channels { + do { + let _ = try await restAPI.ackMessageRead(id: channel.id, msgID: channel.last_message_id ?? "", manual: true, mention_count: 0) + } catch {} + } + } + } + + func copyLink() { + if let guild = guild { + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString( + "https://canary.discord.com/channels/\(guild.id)", + forType: .string + ) + } + } + + func copyId() { + if let guild = guild { + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString( + guild.id, + forType: .string + ) + } + } } diff --git a/Swiftcord/Views/Server/ServerFolder.swift b/Swiftcord/Views/Server/ServerFolder.swift index b2c283cd..91342962 100644 --- a/Swiftcord/Views/Server/ServerFolder.swift +++ b/Swiftcord/Views/Server/ServerFolder.swift @@ -80,7 +80,8 @@ struct ServerFolder: View { Text(folder.name) .font(.title3) .padding(10) - // Prevent popover from blocking clicks to other views + .frame(maxWidth: 300) + // Prevent popover from blocking clicks to other views .interactiveDismissDisabled() } @@ -88,6 +89,7 @@ struct ServerFolder: View { ForEach(folder.guilds, id: \.id) { [self] guild in ServerButton( selected: selectedGuildID == guild.id || loadingGuildID == guild.id, + guild: guild, name: guild.properties.name, serverIconURL: guild.properties.icon != nil ? "\(DiscordKitConfig.default.cdnURL)icons/\(guild.id)/\(guild.properties.icon!).webp?size=240" : nil, isLoading: loadingGuildID == guild.id