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

Reply support #98

Merged
merged 20 commits into from
Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
100 changes: 60 additions & 40 deletions Swiftcord.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

This file was deleted.

7 changes: 3 additions & 4 deletions Swiftcord.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftcordApp/DiscordKit",
"state" : {
"branch" : "main",
"revision" : "f38c9228615275bb20051a0a7bff356e97f45620"
"revision" : "612eef2808f9fd885cdc65fa3f31f4e2a26537e4"
}
},
{
Expand Down Expand Up @@ -59,8 +58,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/lorenzofiamingo/swiftui-cached-async-image",
"state" : {
"revision" : "eeb1565d780d1b75d045e21b5ca2a1e3650b0fc2",
"version" : "2.1.0"
"branch" : "main",
"revision" : "f94be38411297c71aa8df69c6875127e6d8d7a92"
}
},
{
Expand Down
72 changes: 44 additions & 28 deletions Swiftcord/Utils/Extensions/MessagesView+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,30 @@ import DiscordKit
import DiscordKitCommon
import DiscordKitCore

extension MessagesView {
internal func fetchMoreMessages() {
internal extension MessagesView {
func fetchMoreMessages() {
guard let channel = ctx.channel else { return }
if let oldTask = fetchMessagesTask {
if let oldTask = viewModel.fetchMessagesTask {
oldTask.cancel()
fetchMessagesTask = nil
viewModel.fetchMessagesTask = nil
}

if loadError { showingInfoBar = false }
loadError = false
if viewModel.loadError { viewModel.showingInfoBar = false }
viewModel.loadError = false

fetchMessagesTask = Task {
let lastMsg = messages.isEmpty ? nil : messages[messages.count - 1].id
viewModel.fetchMessagesTask = Task {
let lastMsg = viewModel.messages.isEmpty ? nil : viewModel.messages[viewModel.messages.count - 1].id

guard let newMessages = await restAPI.getChannelMsgs(
id: channel.id,
before: lastMsg
) else {
try Task.checkCancellation() // Check if the task is cancelled before continuing

fetchMessagesTask = nil
loadError = true
showingInfoBar = true
infoBarData = InfoBarData(
viewModel.fetchMessagesTask = nil
viewModel.loadError = true
viewModel.showingInfoBar = true
viewModel.infoBarData = InfoBarData(
message: "**Messages failed to load**",
buttonLabel: "Try again",
color: .red,
Expand All @@ -46,25 +46,41 @@ extension MessagesView {
state.loadingState = .messageLoad
try Task.checkCancellation()

reachedTop = newMessages.count < 50
messages.append(contentsOf: newMessages)
fetchMessagesTask = nil
viewModel.reachedTop = newMessages.count < 50
viewModel.messages.append(contentsOf: newMessages)
viewModel.fetchMessagesTask = nil
}
}

internal func sendMessage(with message: String, attachments: [URL]) {
lastSentTyping = Date(timeIntervalSince1970: 0)
newMessage = ""
func sendMessage(with message: String, attachments: [URL]) {
viewModel.lastSentTyping = Date(timeIntervalSince1970: 0)
viewModel.newMessage = ""
viewModel.showingInfoBar = false

// Create message reference if neccessary
var reference: MessageReference? {
if let replying = viewModel.replying {
viewModel.replying = nil // Make sure to clear that
return MessageReference(message_id: replying.messageID, guild_id: replying.guildID)
} else { return nil }
}
var allowedMentions: AllowedMentions? {
if let replying = viewModel.replying {
return AllowedMentions(parse: [.user, .role, .everyone], replied_user: replying.ping)
} else { return nil }
}

if #available(macOS 13, *) {
// Workaround for some race condition, probably a macOS 13 beta bug
DispatchQueue.main.async { newMessage = "" }
} else { newMessage = "" }
showingInfoBar = false
DispatchQueue.main.async { viewModel.newMessage = "" }
} else { viewModel.newMessage = "" }

Task {
guard (await restAPI.createChannelMsg(
message: NewMessage(
content: message,
// message_reference: referencing,
allowed_mentions: allowedMentions,
message_reference: reference,
attachments: attachments.isEmpty ? nil : attachments.enumerated()
.map { (idx, attachment) in
NewAttachment(
Expand All @@ -76,8 +92,8 @@ extension MessagesView {
attachments: attachments,
id: ctx.channel!.id
)) != nil else {
showingInfoBar = true
infoBarData = InfoBarData(
viewModel.showingInfoBar = true
viewModel.infoBarData = InfoBarData(
message: "Could not send message",
buttonLabel: "Try again",
color: .red,
Expand All @@ -89,16 +105,16 @@ extension MessagesView {
}
}

internal func preAttachChecks(for attachment: URL) -> Bool {
func preAttachChecks(for attachment: URL) -> Bool {
guard let size = try? attachment.resourceValues(forKeys: [URLResourceKey.fileSizeKey]).fileSize, size < 8*1024*1024 else {
newAttachmentErr = NewAttachmentError(
viewModel.newAttachmentErr = NewAttachmentError(
title: "Your files are too powerful",
message: "The max file size is 8MB."
)
return false
}
guard attachments.count < 10 else {
newAttachmentErr = NewAttachmentError(
guard viewModel.attachments.count < 10 else {
viewModel.newAttachmentErr = NewAttachmentError(
title: "Too many uploads!",
message: "You can only upload 10 files at a time!"
)
Expand Down
38 changes: 38 additions & 0 deletions Swiftcord/ViewModels/MessagesViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// MessagesViewModel.swift
// Swiftcord
//
// Created by Vincent Kwok on 9/8/22.
//

import SwiftUI
import DiscordKitCommon

extension MessagesView {
// TODO: Make this ViewModel follow best practices and actually function as a ViewModel
@MainActor class ViewModel: ObservableObject {
// For use in the UI - different from MessageReference in DiscordKit
struct ReplyRef {
let messageID: Snowflake
let guildID: Snowflake
let ping: Bool
let authorID: Snowflake
let authorUsername: String
}

@Published var reachedTop = false
@Published var messages: [Message] = []
@Published var newMessage = " "
@Published var attachments: [URL] = []
@Published var showingInfoBar = false
@Published var loadError = false
@Published var infoBarData: InfoBarData?
@Published var fetchMessagesTask: Task<(), Error>?
@Published var lastSentTyping = Date(timeIntervalSince1970: 0)
@Published var newAttachmentErr: NewAttachmentError?
@Published var replying: ReplyRef?
@Published var messageInputHeight = 0.0
@Published var dropOver = false
@Published var highlightMsg: Snowflake?
}
}
58 changes: 58 additions & 0 deletions Swiftcord/Views/Message/MessageInputReplyView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// MessageInputReplyView.swift
// Swiftcord
//
// Created by Vincent Kwok on 11/8/22.
//

import SwiftUI
import DiscordKit
import DiscordKitCommon

struct MessageInputReplyView: View {
@Binding var replying: MessagesView.ViewModel.ReplyRef?

@EnvironmentObject var gateway: DiscordGateway

var body: some View {
if let replyingRef = replying {
HStack(spacing: 12) {
Text("Replying to **\(replyingRef.authorUsername)**")

Spacer()

if replyingRef.authorID != gateway.cache.user?.id {
Button {
withAnimation {
replying = MessagesView.ViewModel.ReplyRef(
messageID: replyingRef.messageID,
guildID: replyingRef.guildID,
ping: !replyingRef.ping,
authorID: replyingRef.authorID,
authorUsername: replyingRef.authorUsername
)
}
} label: {
Label(replyingRef.ping ? "On" : "Off", systemImage: "at")
.foregroundColor(replyingRef.ping ? .blue : .gray)
.font(.system(size: 14, weight: .bold))
}.buttonStyle(.plain)

Divider()
}

Button {
withAnimation {
replying = nil
}
} label: {
Image(systemName: "x.circle.fill").font(.system(size: 16))
}.buttonStyle(.plain)
}
.fixedSize(horizontal: false, vertical: true)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(Color(nsColor: .controlBackgroundColor).opacity(0.3))
}
}
}
18 changes: 12 additions & 6 deletions Swiftcord/Views/Message/MessageInputView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import SwiftUI
import DiscordKitCommon

struct MessageAttachmentView: View {
let attachment: URL
Expand Down Expand Up @@ -56,6 +57,7 @@ struct MessageInputView: View {
let placeholder: LocalizedStringKey
@Binding var message: String
@Binding var attachments: [URL]
@Binding var replying: MessagesView.ViewModel.ReplyRef?
let onSend: (String, [URL]) -> Void
let preAttach: (URL) -> Bool

Expand All @@ -76,6 +78,11 @@ struct MessageInputView: View {

var body: some View {
VStack(alignment: .leading, spacing: 0) {
if replying != nil {
MessageInputReplyView(replying: $replying)
Divider()
}

if !attachments.isEmpty {
ScrollView([.horizontal]) {
HStack {
Expand Down Expand Up @@ -139,13 +146,12 @@ struct MessageInputView: View {
}
}
.frame(minHeight: 40)
.background(RoundedRectangle(cornerRadius: 7, style: .continuous)
.fill(.regularMaterial)
.overlay(
RoundedRectangle(cornerRadius: 7, style: .continuous)
.stroke(Color(nsColor: .separatorColor), lineWidth: 1)
)
.background(.regularMaterial)
.overlay(
RoundedRectangle(cornerRadius: 7)
.strokeBorder(Color(nsColor: .separatorColor), lineWidth: 1)
)
.cornerRadius(7)
.padding(.horizontal, 16)
.offset(y: -24)
}
Expand Down
Loading