From 9e6c3e066a954c5de485e301043ae5eaec51e87e Mon Sep 17 00:00:00 2001 From: 12944qwerty Date: Mon, 24 Jul 2023 14:34:23 -0500 Subject: [PATCH 01/14] reverse list --- Swiftcord/Views/Message/MessagesView.swift | 55 ++++++++++++---------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/Swiftcord/Views/Message/MessagesView.swift b/Swiftcord/Views/Message/MessagesView.swift index 3f7c464f..e5f81301 100644 --- a/Swiftcord/Views/Message/MessagesView.swift +++ b/Swiftcord/Views/Message/MessagesView.swift @@ -169,38 +169,36 @@ struct MessagesView: View { } private var history: some View { - ForEach(Array(viewModel.messages.enumerated()), id: \.1.id) { (idx, msg) in - let isLastItem = idx == viewModel.messages.count-1 - let shrunk = !isLastItem && msg.messageIsShrunk(prev: viewModel.messages[idx+1]) - - cell(for: msg, shrunk: shrunk) + ForEach(Array(viewModel.messages.reversed().enumerated()), id: \.1.id) { (idx, msg) in + let isLastItem = idx == 0 + let shrunk = !isLastItem && msg.messageIsShrunk(prev: viewModel.messages[idx-1]) + if isLastItem && viewModel.reachedTop || !isLastItem && !msg.timestamp.isSameDay(as: viewModel.messages[idx-1].timestamp) { + DayDividerView(date: msg.timestamp) + } + if !isLastItem, let channelID = ctx.channel?.id { - let newMsg = gateway.readState[channelID]?.last_message_id?.stringValue == viewModel.messages[idx+1].id + let newMsg = gateway.readState[channelID]?.last_message_id?.stringValue == viewModel.messages[idx-1].id - if newMsg { UnreadDivider() } if !shrunk && !newMsg { Spacer(minLength: 16 - MessageView.lineSpacing / 2) } + if newMsg { UnreadDivider() } } - - if isLastItem && viewModel.reachedTop || !isLastItem && !msg.timestamp.isSameDay(as: viewModel.messages[idx+1].timestamp) { - DayDividerView(date: msg.timestamp) - } + + cell(for: msg, shrunk: shrunk) + .id(msg.id) } - .flip() .zeroRowInsets() .fixedSize(horizontal: false, vertical: true) } private var historyList: some View { ScrollViewReader { proxy in - List { - Spacer(minLength: max(messageInputHeight-44-7, 0) + (viewModel.showingInfoBar ? 24 : 0)).zeroRowInsets() - - history - + ScrollView { + Spacer(minLength: 52).zeroRowInsets() // Ensure content is fully visible and not hidden behind toolbar when scrolled to the top + if viewModel.reachedTop { - MessagesViewHeader(chl: ctx.channel).zeroRowInsets().flip() + MessagesViewHeader(chl: ctx.channel).zeroRowInsets() } else { loadingSkeleton .zeroRowInsets() @@ -213,17 +211,22 @@ struct MessagesView: View { } } } - - Spacer(minLength: 52).zeroRowInsets() // Ensure content is fully visible and not hidden behind toolbar when scrolled to the top + + history + .onAppear { + withAnimation { + proxy.scrollTo(viewModel.messages.first?.id) + } + } + .padding(.horizontal, 10) + + Spacer(minLength: max(messageInputHeight, 0) + (viewModel.showingInfoBar ? 24 : 0)).zeroRowInsets() } - .introspectTableView { tableView in - tableView.backgroundColor = .clear - tableView.enclosingScrollView!.drawsBackground = false - tableView.enclosingScrollView!.rotate(byDegrees: 180) - tableView.enclosingScrollView!.scrollerInsets = NSEdgeInsets(top: 0, left: 0, bottom: 52, right: 0) + .introspectScrollView { scrollView in + scrollView.drawsBackground = false + scrollView.scrollerInsets = NSEdgeInsets(top: 0, left: 0, bottom: 52, right: 0) } .environment(\.defaultMinListRowHeight, 1) // By SwiftUI's logic, 0 is negative so we use 1 instead - .scaleEffect(x: -1, y: 1, anchor: .center) .background(.clear) .frame(maxHeight: .infinity) .padding(.bottom, 24 + 7) // Ensure List doesn't go below text input field (and its border radius) From 7f4e837faae6ba0462a81f6006e282022f721893 Mon Sep 17 00:00:00 2001 From: 12944qwerty Date: Mon, 24 Jul 2023 14:42:15 -0500 Subject: [PATCH 02/14] fix spacing --- Swiftcord/Views/Message/MessagesView.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Swiftcord/Views/Message/MessagesView.swift b/Swiftcord/Views/Message/MessagesView.swift index e5f81301..40dab7f9 100644 --- a/Swiftcord/Views/Message/MessagesView.swift +++ b/Swiftcord/Views/Message/MessagesView.swift @@ -213,23 +213,24 @@ struct MessagesView: View { } history + .padding(.horizontal, 10) .onAppear { withAnimation { - proxy.scrollTo(viewModel.messages.first?.id) + proxy.scrollTo(1, anchor: .bottom) } } - .padding(.horizontal, 10) - Spacer(minLength: max(messageInputHeight, 0) + (viewModel.showingInfoBar ? 24 : 0)).zeroRowInsets() + Spacer(minLength: max(messageInputHeight-64-7, 5) + (viewModel.showingInfoBar ? 24 : 0)).zeroRowInsets() + .id(1) } .introspectScrollView { scrollView in scrollView.drawsBackground = false - scrollView.scrollerInsets = NSEdgeInsets(top: 0, left: 0, bottom: 52, right: 0) + scrollView.scrollerInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) } .environment(\.defaultMinListRowHeight, 1) // By SwiftUI's logic, 0 is negative so we use 1 instead .background(.clear) .frame(maxHeight: .infinity) - .padding(.bottom, 24 + 7) // Ensure List doesn't go below text input field (and its border radius) + .padding(.bottom, 64 + 7) // Ensure List doesn't go below text input field (and its border radius) } } From ae4560631c86d28e82ee2160c1118d3538774988 Mon Sep 17 00:00:00 2001 From: 12944qwerty Date: Mon, 24 Jul 2023 14:44:10 -0500 Subject: [PATCH 03/14] scroll to reply --- Swiftcord/Views/Message/MessagesView.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Swiftcord/Views/Message/MessagesView.swift b/Swiftcord/Views/Message/MessagesView.swift index 40dab7f9..c57a0976 100644 --- a/Swiftcord/Views/Message/MessagesView.swift +++ b/Swiftcord/Views/Message/MessagesView.swift @@ -146,7 +146,7 @@ struct MessagesView: View { } @_transparent @_optimize(speed) @ViewBuilder - func cell(for msg: Message, shrunk: Bool) -> some View { + func cell(for msg: Message, shrunk: Bool, proxy: ScrollViewProxy) -> some View { MessageView( message: msg, shrunk: shrunk, @@ -155,7 +155,7 @@ struct MessagesView: View { $0.id == msg.message_reference!.message_id } : nil, onQuoteClick: { id in - // withAnimation { proxy.scrollTo(id, anchor: .center) } + withAnimation { proxy.scrollTo(id, anchor: .center) } viewModel.highlightMsg = id DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { if viewModel.highlightMsg == id { viewModel.highlightMsg = nil } @@ -168,7 +168,7 @@ struct MessagesView: View { .listRowBackground(msg.mentions(gateway.cache.user?.id) ? Color.orange.opacity(0.1) : .clear) } - private var history: some View { + func history(proxy: ScrollViewProxy) -> some View { ForEach(Array(viewModel.messages.reversed().enumerated()), id: \.1.id) { (idx, msg) in let isLastItem = idx == 0 let shrunk = !isLastItem && msg.messageIsShrunk(prev: viewModel.messages[idx-1]) @@ -186,7 +186,7 @@ struct MessagesView: View { if newMsg { UnreadDivider() } } - cell(for: msg, shrunk: shrunk) + cell(for: msg, shrunk: shrunk, proxy: proxy) .id(msg.id) } .zeroRowInsets() @@ -212,7 +212,7 @@ struct MessagesView: View { } } - history + history(proxy: proxy) .padding(.horizontal, 10) .onAppear { withAnimation { From c2368c26d1fc48dd30d5c16cf19b915bbd211393 Mon Sep 17 00:00:00 2001 From: 12944qwerty Date: Mon, 24 Jul 2023 15:17:21 -0500 Subject: [PATCH 04/14] fix dividers --- Swiftcord/Views/Message/MessagesView.swift | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Swiftcord/Views/Message/MessagesView.swift b/Swiftcord/Views/Message/MessagesView.swift index c57a0976..577a3b8a 100644 --- a/Swiftcord/Views/Message/MessagesView.swift +++ b/Swiftcord/Views/Message/MessagesView.swift @@ -169,16 +169,17 @@ struct MessagesView: View { } func history(proxy: ScrollViewProxy) -> some View { - ForEach(Array(viewModel.messages.reversed().enumerated()), id: \.1.id) { (idx, msg) in + let reversed = viewModel.messages.reversed() + return ForEach(Array(reversed.enumerated()), id: \.1.id) { (idx, msg) in let isLastItem = idx == 0 - let shrunk = !isLastItem && msg.messageIsShrunk(prev: viewModel.messages[idx-1]) - - if isLastItem && viewModel.reachedTop || !isLastItem && !msg.timestamp.isSameDay(as: viewModel.messages[idx-1].timestamp) { + let shrunk = !isLastItem && msg.messageIsShrunk(prev: reversed.before(msg)!) + + if isLastItem && viewModel.reachedTop || !isLastItem && !msg.timestamp.isSameDay(as: reversed.before(msg)!.timestamp) { DayDividerView(date: msg.timestamp) } if !isLastItem, let channelID = ctx.channel?.id { - let newMsg = gateway.readState[channelID]?.last_message_id?.stringValue == viewModel.messages[idx-1].id + let newMsg = gateway.readState[channelID]?.last_message_id?.stringValue == reversed.before(msg)!.id if !shrunk && !newMsg { Spacer(minLength: 16 - MessageView.lineSpacing / 2) @@ -192,6 +193,7 @@ struct MessagesView: View { .zeroRowInsets() .fixedSize(horizontal: false, vertical: true) } + private var historyList: some View { ScrollViewReader { proxy in ScrollView { @@ -220,7 +222,7 @@ struct MessagesView: View { } } - Spacer(minLength: max(messageInputHeight-64-7, 5) + (viewModel.showingInfoBar ? 24 : 0)).zeroRowInsets() + Spacer(minLength: max(messageInputHeight-74-7, 5) + (viewModel.showingInfoBar ? 24 : 0)).zeroRowInsets() .id(1) } .introspectScrollView { scrollView in @@ -230,7 +232,7 @@ struct MessagesView: View { .environment(\.defaultMinListRowHeight, 1) // By SwiftUI's logic, 0 is negative so we use 1 instead .background(.clear) .frame(maxHeight: .infinity) - .padding(.bottom, 64 + 7) // Ensure List doesn't go below text input field (and its border radius) + .padding(.bottom, 74 + 7) // Ensure List doesn't go below text input field (and its border radius) } } From 18089a6d422917de40fb27d1556a8e36835864ed Mon Sep 17 00:00:00 2001 From: 12944qwerty Date: Mon, 24 Jul 2023 15:32:25 -0500 Subject: [PATCH 05/14] scroll to unread --- Swiftcord/Views/Message/MessagesView.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Swiftcord/Views/Message/MessagesView.swift b/Swiftcord/Views/Message/MessagesView.swift index 577a3b8a..5080af1d 100644 --- a/Swiftcord/Views/Message/MessagesView.swift +++ b/Swiftcord/Views/Message/MessagesView.swift @@ -184,7 +184,10 @@ struct MessagesView: View { if !shrunk && !newMsg { Spacer(minLength: 16 - MessageView.lineSpacing / 2) } - if newMsg { UnreadDivider() } + if newMsg { + UnreadDivider() + .id("unread") + } } cell(for: msg, shrunk: shrunk, proxy: proxy) @@ -204,7 +207,6 @@ struct MessagesView: View { } else { loadingSkeleton .zeroRowInsets() - .flip() .onAppear { if viewModel.fetchMessagesTask == nil { fetchMoreMessages() } } .onDisappear { if let loadTask = viewModel.fetchMessagesTask { @@ -218,12 +220,17 @@ struct MessagesView: View { .padding(.horizontal, 10) .onAppear { withAnimation { - proxy.scrollTo(1, anchor: .bottom) + // Scroll to very bottom if read, otherwise scroll to message + if gateway.readState[ctx.channel?.id ?? "1"]?.last_message_id?.stringValue ?? "1" == viewModel.messages.first?.id ?? "1" { + proxy.scrollTo("1", anchor: .bottom) + } else { + proxy.scrollTo("unread", anchor: .bottom) + } } } Spacer(minLength: max(messageInputHeight-74-7, 5) + (viewModel.showingInfoBar ? 24 : 0)).zeroRowInsets() - .id(1) + .id("1") } .introspectScrollView { scrollView in scrollView.drawsBackground = false From 7fd1d0c0e5e4ca3d3b9e5e4c58e0ab925ec3bd3b Mon Sep 17 00:00:00 2001 From: 12944qwerty Date: Mon, 24 Jul 2023 20:42:10 -0500 Subject: [PATCH 06/14] combine unread and day divider --- Swiftcord/Views/Message/MessagesView.swift | 114 ++++++++++++------ .../Views/Utils/HorizontalDividerView.swift | 4 +- 2 files changed, 77 insertions(+), 41 deletions(-) diff --git a/Swiftcord/Views/Message/MessagesView.swift b/Swiftcord/Views/Message/MessagesView.swift index 5080af1d..dc76fa4c 100644 --- a/Swiftcord/Views/Message/MessagesView.swift +++ b/Swiftcord/Views/Message/MessagesView.swift @@ -111,7 +111,7 @@ struct DayDividerView: View { struct UnreadDivider: View { var body: some View { HStack(spacing: 0) { - Rectangle().fill(.red).frame(height: 1).frame(maxWidth: .infinity) + HorizontalDividerView(color: .red).frame(maxWidth: .infinity) Text("New") .textCase(.uppercase).font(.headline) .padding(.horizontal, 4).padding(.vertical, 2) @@ -121,6 +121,31 @@ struct UnreadDivider: View { } } +struct UnreadDayDividerView: View { + let date: Date + + var body: some View { + + HStack(spacing: 0) { + HStack(spacing: 4) { + HorizontalDividerView(color: .red).frame(maxWidth: .infinity) + Text(date, style: .date) + .font(.system(size: 12)) + .fontWeight(.medium) + .opacity(0.7) + HorizontalDividerView(color: .red).frame(maxWidth: .infinity) + } + .foregroundColor(.red) + Text("New") + .textCase(.uppercase).font(.headline) + .padding(.horizontal, 4).padding(.vertical, 2) + .background(RoundedRectangle(cornerRadius: 4).fill(.red)) + .foregroundColor(.white) + } + .padding(.top, 16) + } +} + struct MessagesView: View { @EnvironmentObject var gateway: DiscordGateway @EnvironmentObject var state: UIState @@ -174,20 +199,27 @@ struct MessagesView: View { let isLastItem = idx == 0 let shrunk = !isLastItem && msg.messageIsShrunk(prev: reversed.before(msg)!) - if isLastItem && viewModel.reachedTop || !isLastItem && !msg.timestamp.isSameDay(as: reversed.before(msg)!.timestamp) { + let newDay = isLastItem && viewModel.reachedTop || !isLastItem && !msg.timestamp.isSameDay(as: reversed.before(msg)!.timestamp) + + var newMsg: Bool { + if !isLastItem, let channelID = ctx.channel?.id { + return gateway.readState[channelID]?.last_message_id?.stringValue == reversed.before(msg)!.id + } + return false + } + + if newDay && newMsg { + UnreadDayDividerView(date: msg.timestamp) + } else if newDay { DayDividerView(date: msg.timestamp) } - if !isLastItem, let channelID = ctx.channel?.id { - let newMsg = gateway.readState[channelID]?.last_message_id?.stringValue == reversed.before(msg)!.id - - if !shrunk && !newMsg { - Spacer(minLength: 16 - MessageView.lineSpacing / 2) - } - if newMsg { - UnreadDivider() - .id("unread") - } + if !shrunk && !newMsg { + Spacer(minLength: 16 - MessageView.lineSpacing / 2) + } + if !newDay && newMsg { + UnreadDivider() + .id("unread") } cell(for: msg, shrunk: shrunk, proxy: proxy) @@ -200,37 +232,39 @@ struct MessagesView: View { private var historyList: some View { ScrollViewReader { proxy in ScrollView { - Spacer(minLength: 52).zeroRowInsets() // Ensure content is fully visible and not hidden behind toolbar when scrolled to the top - - if viewModel.reachedTop { - MessagesViewHeader(chl: ctx.channel).zeroRowInsets() - } else { - loadingSkeleton - .zeroRowInsets() - .onAppear { if viewModel.fetchMessagesTask == nil { fetchMoreMessages() } } - .onDisappear { - if let loadTask = viewModel.fetchMessagesTask { - loadTask.cancel() - viewModel.fetchMessagesTask = nil + Group { +// Spacer(minLength: 52).zeroRowInsets() // Ensure content is fully visible and not hidden behind toolbar when scrolled to the top + + if viewModel.reachedTop { + MessagesViewHeader(chl: ctx.channel).zeroRowInsets() + } else { + loadingSkeleton + .zeroRowInsets() + .onAppear { if viewModel.fetchMessagesTask == nil { fetchMoreMessages() } } + .onDisappear { + if let loadTask = viewModel.fetchMessagesTask { + loadTask.cancel() + viewModel.fetchMessagesTask = nil + } } - } - } - - history(proxy: proxy) - .padding(.horizontal, 10) - .onAppear { - withAnimation { - // Scroll to very bottom if read, otherwise scroll to message - if gateway.readState[ctx.channel?.id ?? "1"]?.last_message_id?.stringValue ?? "1" == viewModel.messages.first?.id ?? "1" { - proxy.scrollTo("1", anchor: .bottom) - } else { - proxy.scrollTo("unread", anchor: .bottom) + } + + history(proxy: proxy) + .onAppear { + withAnimation { + // Scroll to very bottom if read, otherwise scroll to message + if gateway.readState[ctx.channel?.id ?? "1"]?.last_message_id?.stringValue ?? "1" == viewModel.messages.first?.id ?? "1" { + proxy.scrollTo("1", anchor: .bottom) + } else { + proxy.scrollTo("unread", anchor: .bottom) + } } } - } - - Spacer(minLength: max(messageInputHeight-74-7, 5) + (viewModel.showingInfoBar ? 24 : 0)).zeroRowInsets() - .id("1") + + Spacer(minLength: max(messageInputHeight-74-7, 5) + (viewModel.showingInfoBar ? 24 : 0)).zeroRowInsets() + .id("1") + } + .padding(.horizontal, 15) } .introspectScrollView { scrollView in scrollView.drawsBackground = false diff --git a/Swiftcord/Views/Utils/HorizontalDividerView.swift b/Swiftcord/Views/Utils/HorizontalDividerView.swift index 2a4328ee..fe975783 100644 --- a/Swiftcord/Views/Utils/HorizontalDividerView.swift +++ b/Swiftcord/Views/Utils/HorizontalDividerView.swift @@ -8,8 +8,10 @@ import SwiftUI struct HorizontalDividerView: View { + @State var color: Color = Color(NSColor.separatorColor) + var body: some View { - Rectangle().fill(Color(NSColor.separatorColor)).frame(height: 1) + Rectangle().fill(color).frame(height: 1) } } From 78982b7b6770b16c951812a5fe0031b5300fbe64 Mon Sep 17 00:00:00 2001 From: 12944qwerty Date: Mon, 24 Jul 2023 23:24:27 -0500 Subject: [PATCH 07/14] put msgs header to left --- Swiftcord/Views/Message/MessagesView.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Swiftcord/Views/Message/MessagesView.swift b/Swiftcord/Views/Message/MessagesView.swift index dc76fa4c..d3c7e42a 100644 --- a/Swiftcord/Views/Message/MessagesView.swift +++ b/Swiftcord/Views/Message/MessagesView.swift @@ -232,11 +232,11 @@ struct MessagesView: View { private var historyList: some View { ScrollViewReader { proxy in ScrollView { - Group { -// Spacer(minLength: 52).zeroRowInsets() // Ensure content is fully visible and not hidden behind toolbar when scrolled to the top - + Group { if viewModel.reachedTop { - MessagesViewHeader(chl: ctx.channel).zeroRowInsets() + MessagesViewHeader(chl: ctx.channel) + .zeroRowInsets() + .frame(maxWidth: .infinity, alignment: .leading) } else { loadingSkeleton .zeroRowInsets() @@ -272,7 +272,6 @@ struct MessagesView: View { } .environment(\.defaultMinListRowHeight, 1) // By SwiftUI's logic, 0 is negative so we use 1 instead .background(.clear) - .frame(maxHeight: .infinity) .padding(.bottom, 74 + 7) // Ensure List doesn't go below text input field (and its border radius) } } From 2a50864d8b9521c7d9ce23b3164e3ff2c0c2e2ee Mon Sep 17 00:00:00 2001 From: 12944qwerty Date: Tue, 25 Jul 2023 13:13:55 -0500 Subject: [PATCH 08/14] align to bottom --- Swiftcord/Views/Message/MessagesView.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Swiftcord/Views/Message/MessagesView.swift b/Swiftcord/Views/Message/MessagesView.swift index d3c7e42a..0e051a50 100644 --- a/Swiftcord/Views/Message/MessagesView.swift +++ b/Swiftcord/Views/Message/MessagesView.swift @@ -252,6 +252,7 @@ struct MessagesView: View { history(proxy: proxy) .onAppear { withAnimation { + // Already starts at very bottom, but just in case anyway // Scroll to very bottom if read, otherwise scroll to message if gateway.readState[ctx.channel?.id ?? "1"]?.last_message_id?.stringValue ?? "1" == viewModel.messages.first?.id ?? "1" { proxy.scrollTo("1", anchor: .bottom) @@ -261,10 +262,11 @@ struct MessagesView: View { } } - Spacer(minLength: max(messageInputHeight-74-7, 5) + (viewModel.showingInfoBar ? 24 : 0)).zeroRowInsets() + Spacer(minLength: max(messageInputHeight-74, 10) + (viewModel.showingInfoBar ? 24 : 0)).zeroRowInsets() .id("1") } .padding(.horizontal, 15) + .rotationEffect(Angle(degrees: 180)) } .introspectScrollView { scrollView in scrollView.drawsBackground = false @@ -272,7 +274,8 @@ struct MessagesView: View { } .environment(\.defaultMinListRowHeight, 1) // By SwiftUI's logic, 0 is negative so we use 1 instead .background(.clear) - .padding(.bottom, 74 + 7) // Ensure List doesn't go below text input field (and its border radius) + .padding(.top, 74) // Ensure List doesn't go below text input field (and its border radius) + .rotationEffect(Angle(degrees: 180)) } } From 854b099589f79d41c5d01283908f86a54ce34870 Mon Sep 17 00:00:00 2001 From: 12944qwerty Date: Tue, 25 Jul 2023 13:24:27 -0500 Subject: [PATCH 09/14] fix random bug --- Swiftcord/Utils/Extensions/Date+.swift | 4 ++-- Swiftcord/Utils/Extensions/DiscordAPI/Message+.swift | 8 ++++---- Swiftcord/Views/Message/MessagesView.swift | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Swiftcord/Utils/Extensions/Date+.swift b/Swiftcord/Utils/Extensions/Date+.swift index 849f6523..6cc988c6 100644 --- a/Swiftcord/Utils/Extensions/Date+.swift +++ b/Swiftcord/Utils/Extensions/Date+.swift @@ -8,7 +8,7 @@ import Foundation extension Date { - func isSameDay(as date: Date) -> Bool { - Calendar.current.isDate(self, inSameDayAs: date) + func isSameDay(as date: Date?) -> Bool { + date != nil && Calendar.current.isDate(self, inSameDayAs: date!) // won't force abort } } diff --git a/Swiftcord/Utils/Extensions/DiscordAPI/Message+.swift b/Swiftcord/Utils/Extensions/DiscordAPI/Message+.swift index 1a2d1d30..1e3a2b67 100644 --- a/Swiftcord/Utils/Extensions/DiscordAPI/Message+.swift +++ b/Swiftcord/Utils/Extensions/DiscordAPI/Message+.swift @@ -9,10 +9,10 @@ import Foundation import DiscordKitCore extension Message { - func messageIsShrunk(prev: Message) -> Bool { - prev.author.id == self.author.id - && (prev.type == .defaultMsg || prev.type == .reply) + func messageIsShrunk(prev: Message?) -> Bool { + prev?.author.id == self.author.id + && (prev?.type == .defaultMsg || prev?.type == .reply) && self.type == .defaultMsg - && (self.timestamp.timeIntervalSince(prev.timestamp) < 400) + && (self.timestamp.timeIntervalSince(prev!.timestamp) < 400) // won't get to force abort } } diff --git a/Swiftcord/Views/Message/MessagesView.swift b/Swiftcord/Views/Message/MessagesView.swift index 0e051a50..e8d835da 100644 --- a/Swiftcord/Views/Message/MessagesView.swift +++ b/Swiftcord/Views/Message/MessagesView.swift @@ -197,13 +197,13 @@ struct MessagesView: View { let reversed = viewModel.messages.reversed() return ForEach(Array(reversed.enumerated()), id: \.1.id) { (idx, msg) in let isLastItem = idx == 0 - let shrunk = !isLastItem && msg.messageIsShrunk(prev: reversed.before(msg)!) + let shrunk = !isLastItem && msg.messageIsShrunk(prev: reversed.before(msg)) - let newDay = isLastItem && viewModel.reachedTop || !isLastItem && !msg.timestamp.isSameDay(as: reversed.before(msg)!.timestamp) + let newDay = isLastItem && viewModel.reachedTop || !isLastItem && !msg.timestamp.isSameDay(as: reversed.before(msg)?.timestamp) var newMsg: Bool { if !isLastItem, let channelID = ctx.channel?.id { - return gateway.readState[channelID]?.last_message_id?.stringValue == reversed.before(msg)!.id + return gateway.readState[channelID]?.last_message_id?.stringValue == reversed.before(msg)?.id ?? "1" } return false } From f700d2e42793753df578c06c0bc65c69274257a9 Mon Sep 17 00:00:00 2001 From: 12944qwerty Date: Tue, 25 Jul 2023 13:35:34 -0500 Subject: [PATCH 10/14] move scrollbar to right side --- Swiftcord/Views/Message/MessagesView.swift | 82 ++++++++++++---------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/Swiftcord/Views/Message/MessagesView.swift b/Swiftcord/Views/Message/MessagesView.swift index e8d835da..f8859f55 100644 --- a/Swiftcord/Views/Message/MessagesView.swift +++ b/Swiftcord/Views/Message/MessagesView.swift @@ -230,52 +230,56 @@ struct MessagesView: View { } private var historyList: some View { - ScrollViewReader { proxy in - ScrollView { - Group { - if viewModel.reachedTop { - MessagesViewHeader(chl: ctx.channel) - .zeroRowInsets() - .frame(maxWidth: .infinity, alignment: .leading) - } else { - loadingSkeleton - .zeroRowInsets() - .onAppear { if viewModel.fetchMessagesTask == nil { fetchMoreMessages() } } - .onDisappear { - if let loadTask = viewModel.fetchMessagesTask { - loadTask.cancel() - viewModel.fetchMessagesTask = nil + GeometryReader { reader in + ScrollViewReader { proxy in + ScrollView { + Group { + if viewModel.reachedTop { + MessagesViewHeader(chl: ctx.channel) + .zeroRowInsets() + .frame(maxWidth: .infinity, alignment: .leading) + } else { + loadingSkeleton + .zeroRowInsets() + .onAppear { if viewModel.fetchMessagesTask == nil { fetchMoreMessages() } } + .onDisappear { + if let loadTask = viewModel.fetchMessagesTask { + loadTask.cancel() + viewModel.fetchMessagesTask = nil + } } - } - } - - history(proxy: proxy) - .onAppear { - withAnimation { - // Already starts at very bottom, but just in case anyway - // Scroll to very bottom if read, otherwise scroll to message - if gateway.readState[ctx.channel?.id ?? "1"]?.last_message_id?.stringValue ?? "1" == viewModel.messages.first?.id ?? "1" { - proxy.scrollTo("1", anchor: .bottom) - } else { - proxy.scrollTo("unread", anchor: .bottom) + } + + history(proxy: proxy) + .onAppear { + withAnimation { + // Already starts at very bottom, but just in case anyway + // Scroll to very bottom if read, otherwise scroll to message + if gateway.readState[ctx.channel?.id ?? "1"]?.last_message_id?.stringValue ?? "1" == viewModel.messages.first?.id ?? "1" { + proxy.scrollTo("1", anchor: .bottom) + } else { + proxy.scrollTo("unread", anchor: .bottom) + } } } - } + + Spacer(minLength: max(messageInputHeight-74, 10) + (viewModel.showingInfoBar ? 24 : 0)).zeroRowInsets() + .id("1") + } + .padding(.horizontal, 15) + .rotationEffect(Angle(degrees: 180)) + } + .introspectScrollView { scrollView in + scrollView.drawsBackground = false - Spacer(minLength: max(messageInputHeight-74, 10) + (viewModel.showingInfoBar ? 24 : 0)).zeroRowInsets() - .id("1") + // Move to right side, scrollbar is between 15-20 wide + scrollView.scrollerInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: reader.size.width - 15) } - .padding(.horizontal, 15) + .environment(\.defaultMinListRowHeight, 1) // By SwiftUI's logic, 0 is negative so we use 1 instead + .background(.clear) + .padding(.top, 74) // Ensure List doesn't go below text input field (and its border radius) .rotationEffect(Angle(degrees: 180)) } - .introspectScrollView { scrollView in - scrollView.drawsBackground = false - scrollView.scrollerInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - } - .environment(\.defaultMinListRowHeight, 1) // By SwiftUI's logic, 0 is negative so we use 1 instead - .background(.clear) - .padding(.top, 74) // Ensure List doesn't go below text input field (and its border radius) - .rotationEffect(Angle(degrees: 180)) } } From 74219a6f27caa8faad7a8cd1158a370a7dc07441 Mon Sep 17 00:00:00 2001 From: 12944qwerty Date: Thu, 3 Aug 2023 22:44:32 -0500 Subject: [PATCH 11/14] hide scrollbar --- Swiftcord/Views/Message/MessagesView.swift | 84 +++++++++++----------- 1 file changed, 41 insertions(+), 43 deletions(-) diff --git a/Swiftcord/Views/Message/MessagesView.swift b/Swiftcord/Views/Message/MessagesView.swift index f8859f55..7884e140 100644 --- a/Swiftcord/Views/Message/MessagesView.swift +++ b/Swiftcord/Views/Message/MessagesView.swift @@ -230,56 +230,54 @@ struct MessagesView: View { } private var historyList: some View { - GeometryReader { reader in - ScrollViewReader { proxy in - ScrollView { - Group { - if viewModel.reachedTop { - MessagesViewHeader(chl: ctx.channel) - .zeroRowInsets() - .frame(maxWidth: .infinity, alignment: .leading) - } else { - loadingSkeleton - .zeroRowInsets() - .onAppear { if viewModel.fetchMessagesTask == nil { fetchMoreMessages() } } - .onDisappear { - if let loadTask = viewModel.fetchMessagesTask { - loadTask.cancel() - viewModel.fetchMessagesTask = nil - } - } - } - - history(proxy: proxy) - .onAppear { - withAnimation { - // Already starts at very bottom, but just in case anyway - // Scroll to very bottom if read, otherwise scroll to message - if gateway.readState[ctx.channel?.id ?? "1"]?.last_message_id?.stringValue ?? "1" == viewModel.messages.first?.id ?? "1" { - proxy.scrollTo("1", anchor: .bottom) - } else { - proxy.scrollTo("unread", anchor: .bottom) - } + ScrollViewReader { proxy in + ScrollView { + Group { + if viewModel.reachedTop { + MessagesViewHeader(chl: ctx.channel) + .zeroRowInsets() + .frame(maxWidth: .infinity, alignment: .leading) + } else { + loadingSkeleton + .zeroRowInsets() + .onAppear { if viewModel.fetchMessagesTask == nil { fetchMoreMessages() } } + .onDisappear { + if let loadTask = viewModel.fetchMessagesTask { + loadTask.cancel() + viewModel.fetchMessagesTask = nil } } - - Spacer(minLength: max(messageInputHeight-74, 10) + (viewModel.showingInfoBar ? 24 : 0)).zeroRowInsets() - .id("1") } - .padding(.horizontal, 15) - .rotationEffect(Angle(degrees: 180)) - } - .introspectScrollView { scrollView in - scrollView.drawsBackground = false - // Move to right side, scrollbar is between 15-20 wide - scrollView.scrollerInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: reader.size.width - 15) + history(proxy: proxy) + .onAppear { + withAnimation { + // Already starts at very bottom, but just in case anyway + // Scroll to very bottom if read, otherwise scroll to message + if gateway.readState[ctx.channel?.id ?? "1"]?.last_message_id?.stringValue ?? "1" == viewModel.messages.first?.id ?? "1" { + proxy.scrollTo("1", anchor: .bottom) + } else { + proxy.scrollTo("unread", anchor: .bottom) + } + } + } + + Spacer(minLength: max(messageInputHeight-74, 10) + (viewModel.showingInfoBar ? 24 : 0)).zeroRowInsets() + .id("1") } - .environment(\.defaultMinListRowHeight, 1) // By SwiftUI's logic, 0 is negative so we use 1 instead - .background(.clear) - .padding(.top, 74) // Ensure List doesn't go below text input field (and its border radius) + .padding(.horizontal, 15) .rotationEffect(Angle(degrees: 180)) } + .introspectScrollView { scrollView in + scrollView.drawsBackground = false + + // Hide scrollbar + scrollView.scrollerInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: -20) + } + .environment(\.defaultMinListRowHeight, 1) // By SwiftUI's logic, 0 is negative so we use 1 instead + .background(.clear) + .padding(.top, 74) // Ensure List doesn't go below text input field (and its border radius) + .rotationEffect(Angle(degrees: 180)) } } From 338a684d1a559daeda6db8f5ed6bc8020019450d Mon Sep 17 00:00:00 2001 From: vinkwok Date: Mon, 11 Sep 2023 19:04:39 +0800 Subject: [PATCH 12/14] Squashed commit of the following: commit e6f8bde72685c796304662ee21ff4acd98b9eb61 Author: vinkwok Date: Sat Jul 1 13:44:23 2023 +0800 fix(onboarding): move attributed title declaration out of view body commit 952ddb93dd6923e6d302f286aadde1f398d0d5f5 Author: vinkwok Date: Fri Jun 30 12:16:00 2023 +0800 fix: remove list dividers added in Xcode 15 commit 0684932f1d218a2734162ee0427f598f65fd7ddd Author: vinkwok Date: Fri Jun 30 12:15:24 2023 +0800 feat(permissions): channel visibility based on permissions --- Swiftcord.xcodeproj/project.pbxproj | 6 ++ .../xcshareddata/swiftpm/Package.resolved | 60 ++++++++++++++++- Swiftcord/AppDelegate.swift | 10 +++ .../Extensions/DiscordAPI/Permissions+.swift | 18 +++++ Swiftcord/Views/ContentView.swift | 12 ++-- .../Message/Attachment/AttachmentAudio.swift | 2 +- .../DefaultMessageView.swift | 2 +- Swiftcord/Views/Message/MessagesView.swift | 15 ++++- Swiftcord/Views/OnboardingView.swift | 24 +++---- Swiftcord/Views/Server/ChannelButton.swift | 2 +- Swiftcord/Views/Server/ChannelList.swift | 66 +++++++++++++++---- Swiftcord/Views/Server/ServerFolder.swift | 16 ++--- Swiftcord/Views/Server/ServerView.swift | 50 +++++++++++--- 13 files changed, 226 insertions(+), 57 deletions(-) create mode 100644 Swiftcord/Utils/Extensions/DiscordAPI/Permissions+.swift diff --git a/Swiftcord.xcodeproj/project.pbxproj b/Swiftcord.xcodeproj/project.pbxproj index bb2b5f40..32c7ad50 100644 --- a/Swiftcord.xcodeproj/project.pbxproj +++ b/Swiftcord.xcodeproj/project.pbxproj @@ -184,6 +184,8 @@ DA7721FD2896BD4D0007BE26 /* URL+.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA7721FC2896BD4D0007BE26 /* URL+.swift */; }; DA7721FE2896BD4D0007BE26 /* URL+.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA7721FC2896BD4D0007BE26 /* URL+.swift */; }; DA8AEA6829029747007BAAEA /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = DA8AEA6729029747007BAAEA /* Introspect */; }; + DA9029572A0CC450008E05FA /* Permissions+.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9029562A0CC450008E05FA /* Permissions+.swift */; }; + DA9029582A0CC454008E05FA /* Permissions+.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9029562A0CC450008E05FA /* Permissions+.swift */; }; DA91016C28BF8F1F00DD076B /* CreditsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB1E46928A0A59500645FCD /* CreditsView.swift */; }; DA91017028C38E0400DD076B /* CurrentUserFooter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA91016F28C38E0400DD076B /* CurrentUserFooter+.swift */; }; DA91017128C38E0400DD076B /* CurrentUserFooter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA91016F28C38E0400DD076B /* CurrentUserFooter+.swift */; }; @@ -352,6 +354,7 @@ DA6E89EF2876BC7E00BB05E7 /* AppSettingsAdvancedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsAdvancedView.swift; sourceTree = ""; }; DA7720CF283F184100D3C335 /* NavigationCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCommands.swift; sourceTree = ""; }; DA7721FC2896BD4D0007BE26 /* URL+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+.swift"; sourceTree = ""; }; + DA9029562A0CC450008E05FA /* Permissions+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Permissions+.swift"; sourceTree = ""; }; DA91016F28C38E0400DD076B /* CurrentUserFooter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CurrentUserFooter+.swift"; sourceTree = ""; }; DA91017228C38EA300DD076B /* AccountRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRow.swift; sourceTree = ""; }; DA91017528C3989E00DD076B /* AccountMeta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMeta.swift; sourceTree = ""; }; @@ -831,6 +834,7 @@ DA32EF5127C8FBB200A9ED72 /* User+.swift */, DA54D5752844B9C500B11857 /* CurrentUser+.swift */, DAC437782900F3FD00D3A894 /* Snowflake+.swift */, + DA9029562A0CC450008E05FA /* Permissions+.swift */, ); path = DiscordAPI; sourceTree = ""; @@ -1184,6 +1188,7 @@ 36429989286801C900483D0A /* UserSettingsProfileView.swift in Sources */, DA91017A28C4726300DD076B /* AccountSwitcher.swift in Sources */, 3642998A286801C900483D0A /* ServerView.swift in Sources */, + DA9029582A0CC454008E05FA /* Permissions+.swift in Sources */, 3642998B286801C900483D0A /* SwiftcordApp.swift in Sources */, 368B6730287A20F800E37B33 /* ServerJoinView.swift in Sources */, ); @@ -1270,6 +1275,7 @@ DAB1E46C28A10BB100645FCD /* BetterImageView.swift in Sources */, DAA57E242892270800C9A931 /* SwiftyGifNSView.swift in Sources */, 9FCE7B1D28C7140100213A3F /* ServerFolder.swift in Sources */, + DA9029572A0CC450008E05FA /* Permissions+.swift in Sources */, DA2BD30C284CB38B00EBB8D6 /* AppSettingsAppearanceView.swift in Sources */, DAAFB5C3282AA5C700807B54 /* MessageInfoBarView.swift in Sources */, DA32EF5027C8D7E000A9ED72 /* Message+.swift in Sources */, diff --git a/Swiftcord.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Swiftcord.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 44039584..9998b1ac 100644 --- a/Swiftcord.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Swiftcord.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -9,13 +9,22 @@ "revision" : "2214f9ee2476f28af64cb38359defe59e85197a1" } }, + { + "identity" : "bitbytedata", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tsolomko/BitByteData", + "state" : { + "revision" : "36df26fe4586b4f23d76cfd8b47076998343a2b2", + "version" : "2.0.3" + } + }, { "identity" : "discordkit", "kind" : "remoteSourceControl", "location" : "https://github.com/SwiftcordApp/DiscordKit", "state" : { "branch" : "main", - "revision" : "dc16b007a5dbfdf008c51897257af36713e91c19" + "revision" : "1474f71dabb18050cc2422d08643731b7aecf6a4" } }, { @@ -27,6 +36,15 @@ "version" : "4.1.3" } }, + { + "identity" : "opencombine", + "kind" : "remoteSourceControl", + "location" : "https://github.com/OpenCombine/OpenCombine.git", + "state" : { + "revision" : "8576f0d579b27020beccbccc3ea6844f3ddfc2c2", + "version" : "0.14.0" + } + }, { "identity" : "plcrashreporter", "kind" : "remoteSourceControl", @@ -63,6 +81,15 @@ "version" : "2.3.2" } }, + { + "identity" : "swcompression", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tsolomko/SWCompression.git", + "state" : { + "revision" : "cd39ca0a3b269173bab06f68b182b72fa690765c", + "version" : "4.8.5" + } + }, { "identity" : "swift-log", "kind" : "remoteSourceControl", @@ -72,13 +99,31 @@ "version" : "1.5.2" } }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "124119f0bb12384cef35aa041d7c3a686108722d", + "version" : "2.40.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "1750873bce84b4129b5303655cce2c3d35b9ed3a", + "version" : "2.19.0" + } + }, { "identity" : "swift-protobuf", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "0af9125c4eae12a4973fb66574c53a54962a9e1e", - "version" : "1.21.0" + "revision" : "f25867a208f459d3c5a06935dceb9083b11cd539", + "version" : "1.22.0" } }, { @@ -107,6 +152,15 @@ "branch" : "fix-timer-crash", "revision" : "32a6bd02c9c44c8cdd4e98f46037678ee1abecbb" } + }, + { + "identity" : "websocket.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tesseract-one/WebSocket.swift.git", + "state" : { + "revision" : "9f616c35127c83651d3112f8bdb41284d3c5c213", + "version" : "0.2.0" + } } ], "version" : 2 diff --git a/Swiftcord/AppDelegate.swift b/Swiftcord/AppDelegate.swift index 4fc805f9..9c009bc4 100644 --- a/Swiftcord/AppDelegate.swift +++ b/Swiftcord/AppDelegate.swift @@ -61,6 +61,16 @@ private extension AppDelegate { private extension AppDelegate { /// Overwrite shared URLCache with a higher capacity one func setupURLCache() { + /*let cachePath = (try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true))?.appendingPathComponent("sharedCache", isDirectory: false) + if let cachePath { + do { + try FileManager.default.createDirectory(at: cachePath, withIntermediateDirectories: true) + } catch { + print("Create new cache dir fail! \(error)") + return + } + } + print("Cache path: \(cachePath)")*/ URLCache.shared = URLCache( memoryCapacity: 32 * 1024 * 1024, // 32MB diskCapacity: 256 * 1024 * 1024, // 256MB diff --git a/Swiftcord/Utils/Extensions/DiscordAPI/Permissions+.swift b/Swiftcord/Utils/Extensions/DiscordAPI/Permissions+.swift new file mode 100644 index 00000000..ff78a8d2 --- /dev/null +++ b/Swiftcord/Utils/Extensions/DiscordAPI/Permissions+.swift @@ -0,0 +1,18 @@ +// +// Permissions+.swift +// Swiftcord +// +// Created by Vincent Kwok on 11/5/23. +// + +import Foundation +import DiscordKitCore + +public extension Permissions { + static let all: Permissions = .init(rawValue: 0x7FFFFFFFFFFF) + + mutating func applyOverwrite(_ overwrite: PermOverwrite) { + remove(overwrite.deny) + formUnion(overwrite.allow) + } +} diff --git a/Swiftcord/Views/ContentView.swift b/Swiftcord/Views/ContentView.swift index d47f3d59..d334c8cd 100644 --- a/Swiftcord/Views/ContentView.swift +++ b/Swiftcord/Views/ContentView.swift @@ -65,14 +65,14 @@ struct ContentView: View { folder.guild_ids.contains(guild.id) } } - .sorted { lhs, rhs in lhs.joined_at! > rhs.joined_at! } + .sorted { lhs, rhs in lhs.joined_at > rhs.joined_at } .map { ServerListItem.guild($0) } return unsortedGuilds + gateway.guildFolders.compactMap { folder -> ServerListItem? in if folder.id != nil { let guilds = folder.guild_ids.compactMap { gateway.cache.guilds[$0] } - let name = folder.name ?? String(guilds.map { $0.name }.joined(separator: ", ")) + let name = folder.name ?? String(guilds.map { $0.properties.name }.joined(separator: ", ")) return .guildFolder(ServerFolder.GuildFolder( name: name, guilds: guilds, color: folder.color.flatMap { Color(hex: $0) } ?? Color.accentColor )) @@ -105,8 +105,8 @@ struct ContentView: View { case .guild(let guild): ServerButton( selected: state.selectedGuildID == guild.id || loadingGuildID == guild.id, - name: guild.name, - serverIconURL: guild.icon != nil ? "\(DiscordKitConfig.default.cdnURL)icons/\(guild.id)/\(guild.icon!).webp?size=240" : nil, + 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, onSelect: { state.selectedGuildID = guild.id } ) @@ -142,7 +142,7 @@ struct ContentView: View { ServerView( guild: state.selectedGuildID == nil ? nil - : (state.selectedGuildID == "@me" ? makeDMGuild() : gateway.cache.guilds[state.selectedGuildID!]), serverCtx: state.serverCtx + : ( gateway.cache.guilds[state.selectedGuildID!]), serverCtx: state.serverCtx ) } // Blur the area behind the toolbar so the content doesn't show thru @@ -217,7 +217,7 @@ struct ContentView: View { } private enum ServerListItem: Identifiable { - case guild(Guild), guildFolder(ServerFolder.GuildFolder) + case guild(PreloadedGuild), guildFolder(ServerFolder.GuildFolder) var id: String { switch self { diff --git a/Swiftcord/Views/Message/Attachment/AttachmentAudio.swift b/Swiftcord/Views/Message/Attachment/AttachmentAudio.swift index 87066420..e7a38906 100644 --- a/Swiftcord/Views/Message/Attachment/AttachmentAudio.swift +++ b/Swiftcord/Views/Message/Attachment/AttachmentAudio.swift @@ -19,7 +19,7 @@ struct AttachmentAudio: View { audioManager.append( source: url, filename: attachment.filename, - from: "\(serverCtx.guild!.name) > #\(serverCtx.channel?.name ?? "")" + from: "\(serverCtx.guild!.properties.name) > #\(serverCtx.channel?.name ?? "")" ) } diff --git a/Swiftcord/Views/Message/MessageRenderViews/DefaultMessageView.swift b/Swiftcord/Views/Message/MessageRenderViews/DefaultMessageView.swift index b84d63ec..e6c52aa6 100644 --- a/Swiftcord/Views/Message/MessageRenderViews/DefaultMessageView.swift +++ b/Swiftcord/Views/Message/MessageRenderViews/DefaultMessageView.swift @@ -38,7 +38,7 @@ struct DefaultMessageView: View { .italic() .foregroundColor(Color(NSColor.textColor).opacity(0.4)) } - .lineSpacing(3) + .lineSpacing(4) .textSelection(.enabled) } if let stickerItems = message.sticker_items { diff --git a/Swiftcord/Views/Message/MessagesView.swift b/Swiftcord/Views/Message/MessagesView.swift index 7884e140..c22b1a22 100644 --- a/Swiftcord/Views/Message/MessagesView.swift +++ b/Swiftcord/Views/Message/MessagesView.swift @@ -20,6 +20,16 @@ extension View { } } +extension View { + @ViewBuilder public func removeSeparator() -> some View { + if #available(macOS 13.0, *) { + self.listRowSeparator(.hidden).listSectionSeparator(.hidden) + } else { + self + } + } +} + struct NewAttachmentError: Identifiable { var id: String { title + message } let title: String @@ -228,7 +238,8 @@ struct MessagesView: View { .zeroRowInsets() .fixedSize(horizontal: false, vertical: true) } - + + @ViewBuilder private var historyList: some View { ScrollViewReader { proxy in ScrollView { @@ -311,7 +322,7 @@ struct MessagesView: View { let typingMembers = ctx.channel == nil ? [] : ctx.typingStarted[ctx.channel!.id]? - .map { $0.member?.nick ?? $0.member?.user!.username ?? "" } ?? [] + .map { $0.member?.nick ?? $0.member?.user?.username ?? "" } ?? [] if !typingMembers.isEmpty { HStack { diff --git a/Swiftcord/Views/OnboardingView.swift b/Swiftcord/Views/OnboardingView.swift index d6adb17d..60991bde 100644 --- a/Swiftcord/Views/OnboardingView.swift +++ b/Swiftcord/Views/OnboardingView.swift @@ -13,22 +13,22 @@ struct OnboardingWelcomeView: View { let loadingNew: Bool let hasNew: Bool - var body: some View { - VStack(alignment: .leading, spacing: 16) { - Group { - var attributedTitle: AttributedString { - var attributedString: AttributedString = .init(localized: "onboarding.title \(appName ?? "")") + var attributedTitle: AttributedString { + var attributedString: AttributedString = .init(localized: "onboarding.title \(appName ?? "")") - let appNameRange = attributedString.range(of: appName ?? "") + let appNameRange = attributedString.range(of: appName ?? "") - if let appNameRange = appNameRange { - attributedString[appNameRange].foregroundColor = .accentColor - attributedString[appNameRange].font = .system(size: 72).weight(.heavy) - } + if let appNameRange = appNameRange { + attributedString[appNameRange].foregroundColor = .accentColor + attributedString[appNameRange].font = .system(size: 72).weight(.heavy) + } - return attributedString - } + return attributedString + } + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Group { Text(attributedTitle) .font(.largeTitle) diff --git a/Swiftcord/Views/Server/ChannelButton.swift b/Swiftcord/Views/Server/ChannelButton.swift index fd304b95..c6662a7a 100644 --- a/Swiftcord/Views/Server/ChannelButton.swift +++ b/Swiftcord/Views/Server/ChannelButton.swift @@ -43,7 +43,7 @@ struct GuildChButton: View { var body: some View { Button { selectedCh = channel } label: { - let image = (serverCtx.guild?.rules_channel_id != nil && serverCtx.guild?.rules_channel_id! == channel.id) ? "newspaper.fill" : (chIcons[channel.type] ?? "number") + let image = serverCtx.guild?.properties.rules_channel_id == channel.id ? "newspaper.fill" : (chIcons[channel.type] ?? "number") Label(channel.label() ?? "nil", systemImage: image) .padding(.vertical, 5) .padding(.horizontal, 4) diff --git a/Swiftcord/Views/Server/ChannelList.swift b/Swiftcord/Views/Server/ChannelList.swift index b16497b4..8ca0290a 100644 --- a/Swiftcord/Views/Server/ChannelList.swift +++ b/Swiftcord/Views/Server/ChannelList.swift @@ -11,7 +11,7 @@ import DiscordKitCore import DiscordKit /// Renders the channel list on the sidebar -struct ChannelList: View { +struct ChannelList: View, Equatable { let channels: [Channel] @Binding var selCh: Channel? @AppStorage("nsfwShown") var nsfwShown: Bool = true @@ -31,19 +31,58 @@ struct ChannelList: View { }) } + private static func computeOverwrites( + channel: Channel, guildID: Snowflake, + member: Member, basePerms: Permissions + ) -> Permissions { + if basePerms.contains(.administrator) { + return .all + } + var permission = basePerms + // Apply the overwrite for the @everyone permission + if let everyoneOverwrite = channel.permission_overwrites?.first(where: { $0.id == guildID }) { + permission.applyOverwrite(everyoneOverwrite) + } + // Next, apply role-specific overwrites + channel.permission_overwrites?.forEach { overwrite in + if member.roles.contains(overwrite.id) { + permission.applyOverwrite(overwrite) + } + } + // Finally, apply member-specific overwrites - must be done after all roles + channel.permission_overwrites?.forEach { overwrite in + if member.user_id == overwrite.id { + permission.applyOverwrite(overwrite) + } + } + return permission + } + var body: some View { + let availableChs = channels.filter { channel in + guard let guildID = serverCtx.guild?.id, let member = serverCtx.member else { + // print("no guild or member!") + return true + } + guard channel.type != .category else { + return true + } + return Self.computeOverwrites( + channel: channel, + guildID: guildID, + member: member, basePerms: serverCtx.basePermissions + ) + .contains(.viewChannel) + } List { Spacer(minLength: 52 - 16 + 4) // 52 (header) - 16 (unremovable section top padding) + 4 (spacing) - let filteredChannels = channels.filter { - if !nsfwShown { - return $0.parent_id == nil && $0.type != .category && ($0.nsfw == false || $0.nsfw == nil) - } - return $0.parent_id == nil && $0.type != .category + let filteredChannels = availableChs.filter { + $0.parent_id == nil && $0.type != .category && (nsfwShown || ($0.nsfw == false || $0.nsfw == nil)) } if !filteredChannels.isEmpty { Section( - header: Text(serverCtx.guild?.isDMChannel == true + header: Text(serverCtx.guild?.properties.isDMChannel == true ? "dm" : "server.channel.noCategory" ).textCase(.uppercase).padding(.leading, 8) @@ -53,16 +92,13 @@ struct ChannelList: View { } } - let categoryChannels = channels + let categoryChannels = availableChs .filter { $0.parent_id == nil && $0.type == .category } .discordSorted() ForEach(categoryChannels, id: \.id) { channel in // Channels in this section - let channels = channels.filter { - if !nsfwShown { - return $0.parent_id == channel.id && ($0.nsfw == false || $0.nsfw == nil) - } - return $0.parent_id == channel.id + let channels = availableChs.filter { + $0.parent_id == channel.id && (nsfwShown || ($0.nsfw == false || $0.nsfw == nil)) }.discordSorted() if !channels.isEmpty { Section(header: Text(channel.name ?? "").textCase(.uppercase).padding(.leading, 8)) { @@ -82,4 +118,8 @@ struct ChannelList: View { } .environment(\.defaultMinListRowHeight, 1) } + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.channels == rhs.channels && lhs.selCh == rhs.selCh + } } diff --git a/Swiftcord/Views/Server/ServerFolder.swift b/Swiftcord/Views/Server/ServerFolder.swift index 43f25b7a..b2c283cd 100644 --- a/Swiftcord/Views/Server/ServerFolder.swift +++ b/Swiftcord/Views/Server/ServerFolder.swift @@ -80,7 +80,7 @@ struct ServerFolder: View { Text(folder.name) .font(.title3) .padding(10) - // Prevent popover from blocking clicks to other views + // Prevent popover from blocking clicks to other views .interactiveDismissDisabled() } @@ -88,8 +88,8 @@ struct ServerFolder: View { ForEach(folder.guilds, id: \.id) { [self] guild in ServerButton( selected: selectedGuildID == guild.id || loadingGuildID == guild.id, - name: guild.name, - serverIconURL: guild.icon != nil ? "\(DiscordKitConfig.default.cdnURL)icons/\(guild.id)/\(guild.icon!).webp?size=240" : nil, + 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 ) { selectedGuildID = guild.id @@ -112,7 +112,7 @@ struct ServerFolder: View { struct GuildFolder: Identifiable { let name: String - let guilds: [Guild] + let guilds: [PreloadedGuild] let color: Color var id: Snowflake { @@ -124,7 +124,7 @@ struct ServerFolder: View { struct ServerFolderButtonStyle: ButtonStyle { let open: Bool let color: Color - let guilds: [Guild] + let guilds: [PreloadedGuild] @Binding var hovered: Bool func makeBody(configuration: Configuration) -> some View { @@ -170,11 +170,11 @@ struct ServerFolderButtonStyle: ButtonStyle { } struct MiniServerThumb: View { - let guild: Guild + let guild: PreloadedGuild let animate: Bool var body: some View { - if let serverIconPath = guild.icon, let iconURL = URL(string: "\(DiscordKitConfig.default.cdnURL)icons/\(guild.id)/\(serverIconPath).webp?size=240") { + if let serverIconPath = guild.properties.icon, let iconURL = URL(string: "\(DiscordKitConfig.default.cdnURL)icons/\(guild.id)/\(serverIconPath).webp?size=240") { if iconURL.isAnimatable { SwiftyGifView( url: iconURL.modifyingPathExtension("gif"), @@ -190,7 +190,7 @@ struct MiniServerThumb: View { .cornerRadius(8) } } else { - let iconName = guild.name.split(separator: " ").map { $0.prefix(1) }.joined(separator: "") + let iconName = guild.properties.name.split(separator: " ").map { $0.prefix(1) }.joined(separator: "") Text(iconName) .font(.system(size: 8)) .lineLimit(1) diff --git a/Swiftcord/Views/Server/ServerView.swift b/Swiftcord/Views/Server/ServerView.swift index 628e8261..fb58e63e 100644 --- a/Swiftcord/Views/Server/ServerView.swift +++ b/Swiftcord/Views/Server/ServerView.swift @@ -11,13 +11,15 @@ import DiscordKitCore class ServerContext: ObservableObject { @Published public var channel: Channel? - @Published public var guild: Guild? + @Published public var guild: PreloadedGuild? @Published public var typingStarted: [Snowflake: [TypingStart]] = [:] @Published public var roles: [Role] = [] + @Published public var basePermissions: Permissions = .init() + @Published public var member: Member? } struct ServerView: View { - let guild: Guild? + let guild: PreloadedGuild? @State private var evtID: EventDispatch.HandlerIdentifier? @State private var mediaCenterOpen: Bool = false @@ -29,7 +31,7 @@ struct ServerView: View { private func loadChannels() { guard state.loadingState != .initial else { return } // Ensure gateway is connected before loading anything - guard let channels = serverCtx.guild?.channels?.discordSorted() + guard let channels = serverCtx.guild?.channels.discordSorted() else { return } if let lastChannel = UserDefaults.standard.string(forKey: "lastCh.\(serverCtx.guild!.id)"), @@ -44,28 +46,54 @@ struct ServerView: View { if serverCtx.channel == nil { state.loadingState = .messageLoad } } - private func bootstrapGuild(with guild: Guild) { + private static func computeBasePermissions( + for member: Member, + guild: PreloadedGuild, guildRoles: [Role] + ) -> Permissions { + if member.user_id == guild.properties.owner_id { + return .all + } + guard var basePerms = guildRoles.first(where: { $0.id == guild.id })?.permissions else { + return .init() + } + member.roles.forEach { roleID in + if let role = guildRoles.first(where: { $0.id == roleID }) { + basePerms.formUnion(role.permissions) + } + } + return basePerms + } + + private func bootstrapGuild(with guild: PreloadedGuild) { serverCtx.guild = guild serverCtx.roles = [] + serverCtx.basePermissions = .init() loadChannels() // Sending malformed IDs causes an instant Gateway session termination - guard !guild.isDMChannel else { + guard !guild.properties.isDMChannel else { AnalyticsWrapper.event(type: .DMListViewed, properties: [ "channel_id": serverCtx.channel?.id ?? "", "channel_type": serverCtx.channel?.type.rawValue ?? 1 ]) + serverCtx.basePermissions = .all return } AnalyticsWrapper.event(type: .guildViewed, properties: [ "guild_id": guild.id, - "guild_is_vip": guild.premium_tier != PremiumLevel.none, - "guild_num_channels": guild.channels?.count ?? 0 + "guild_is_vip": guild.premium_subscription_count > 0, + "guild_num_channels": guild.channels.count ]) // Subscribe to typing events gateway.subscribeGuildEvents(id: guild.id) serverCtx.roles = guild.roles.compactMap { role in try? role.result.get() } + serverCtx.member = gateway.cache.members[guild.id] + // print(guild.roles) + guard let member = serverCtx.member else { return } + print(member) + serverCtx.basePermissions = Self.computeBasePermissions(for: member, guild: guild, guildRoles: serverCtx.roles) + print(serverCtx.basePermissions) // Retrieve guild roles to update context /*Task { guard let newRoles = await restAPI.getGuildRoles(id: guild.id) else { return } @@ -79,14 +107,16 @@ struct ServerView: View { } var body: some View { + let _ = print("rerender server") NavigationView { // MARK: Channel List VStack(spacing: 0) { if let guild = guild { - ChannelList(channels: guild.name == "DMs" ? gateway.cache.dms : guild.channels!, selCh: $serverCtx.channel) + ChannelList(channels: guild.properties.name == "DMs" ? gateway.cache.dms : guild.channels, selCh: $serverCtx.channel) + .equatable() .toolbar { ToolbarItem { - Text(guild.name == "DMs" ? "dm" : "\(guild.name)") + Text(guild.properties.name == "DMs" ? "dm" : "\(guild.properties.name)") .font(.title3) .fontWeight(.semibold) .frame(maxWidth: 208) // Largest width before disappearing @@ -120,7 +150,7 @@ struct ServerView: View { } // MARK: Message History - if serverCtx.channel != nil { + if serverCtx.channel != nil, serverCtx.guild != nil { MessagesView() } else { VStack(spacing: 24) { From f91be00bb59978497245b22d8badade520b87e68 Mon Sep 17 00:00:00 2001 From: 12944qwerty Date: Mon, 11 Sep 2023 10:18:41 -0400 Subject: [PATCH 13/14] use List instead of ScrollView --- .../xcshareddata/swiftpm/Package.resolved | 8 +- Swiftcord/Views/Message/MessagesView.swift | 81 ++++++++++--------- 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/Swiftcord.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Swiftcord.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9998b1ac..d5a03498 100644 --- a/Swiftcord.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Swiftcord.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "32e8d724467f8fe623624570367e3d50c5638e46", - "version" : "1.5.2" + "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", + "version" : "1.5.3" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "f25867a208f459d3c5a06935dceb9083b11cd539", - "version" : "1.22.0" + "revision" : "cf62cdaea48b77f1a631e5cb3aeda6047c2cba1d", + "version" : "1.23.0" } }, { diff --git a/Swiftcord/Views/Message/MessagesView.swift b/Swiftcord/Views/Message/MessagesView.swift index c22b1a22..1dcf3da2 100644 --- a/Swiftcord/Views/Message/MessagesView.swift +++ b/Swiftcord/Views/Message/MessagesView.swift @@ -135,7 +135,6 @@ struct UnreadDayDividerView: View { let date: Date var body: some View { - HStack(spacing: 0) { HStack(spacing: 4) { HorizontalDividerView(color: .red).frame(maxWidth: .infinity) @@ -204,36 +203,36 @@ struct MessagesView: View { } func history(proxy: ScrollViewProxy) -> some View { - let reversed = viewModel.messages.reversed() - return ForEach(Array(reversed.enumerated()), id: \.1.id) { (idx, msg) in - let isLastItem = idx == 0 - let shrunk = !isLastItem && msg.messageIsShrunk(prev: reversed.before(msg)) + let messages = viewModel.messages + return ForEach(Array(messages.enumerated()), id: \.1.id) { (idx, msg) in + let isLastItem = msg.id == messages.last?.id + let shrunk = !isLastItem && msg.messageIsShrunk(prev: messages.after(msg)) - let newDay = isLastItem && viewModel.reachedTop || !isLastItem && !msg.timestamp.isSameDay(as: reversed.before(msg)?.timestamp) + let newDay = isLastItem && viewModel.reachedTop || !isLastItem && !msg.timestamp.isSameDay(as: messages.after(msg)?.timestamp) var newMsg: Bool { if !isLastItem, let channelID = ctx.channel?.id { - return gateway.readState[channelID]?.last_message_id?.stringValue == reversed.before(msg)?.id ?? "1" + return gateway.readState[channelID]?.last_message_id?.stringValue == messages.after(msg)?.id ?? "1" } return false } - if newDay && newMsg { - UnreadDayDividerView(date: msg.timestamp) - } else if newDay { - DayDividerView(date: msg.timestamp) - } + cell(for: msg, shrunk: shrunk, proxy: proxy) + .id(msg.id) - if !shrunk && !newMsg { - Spacer(minLength: 16 - MessageView.lineSpacing / 2) - } if !newDay && newMsg { UnreadDivider() .id("unread") } + if !shrunk && !newMsg { + Spacer(minLength: 16 - MessageView.lineSpacing / 2) + } - cell(for: msg, shrunk: shrunk, proxy: proxy) - .id(msg.id) + if newDay && newMsg { + UnreadDayDividerView(date: msg.timestamp) + } else if newDay { + DayDividerView(date: msg.timestamp) + } } .zeroRowInsets() .fixedSize(horizontal: false, vertical: true) @@ -242,8 +241,25 @@ struct MessagesView: View { @ViewBuilder private var historyList: some View { ScrollViewReader { proxy in - ScrollView { + List { Group { + Spacer(minLength: max(messageInputHeight-74, 10) + (viewModel.showingInfoBar ? 24 : 0)).zeroRowInsets() + .id("1") + + history(proxy: proxy) + .onAppear { + withAnimation { + // Already starts at very bottom, but just in case anyway + // Scroll to very bottom if read, otherwise scroll to message + if gateway.readState[ctx.channel?.id ?? "1"]?.last_message_id?.stringValue ?? "1" == viewModel.messages.first?.id ?? "1" { + proxy.scrollTo("1", anchor: .bottom) + } else { + proxy.scrollTo("unread", anchor: .bottom) + } + } + } + + if viewModel.reachedTop { MessagesViewHeader(chl: ctx.channel) .zeroRowInsets() @@ -259,35 +275,20 @@ struct MessagesView: View { } } } - - history(proxy: proxy) - .onAppear { - withAnimation { - // Already starts at very bottom, but just in case anyway - // Scroll to very bottom if read, otherwise scroll to message - if gateway.readState[ctx.channel?.id ?? "1"]?.last_message_id?.stringValue ?? "1" == viewModel.messages.first?.id ?? "1" { - proxy.scrollTo("1", anchor: .bottom) - } else { - proxy.scrollTo("unread", anchor: .bottom) - } - } - } - - Spacer(minLength: max(messageInputHeight-74, 10) + (viewModel.showingInfoBar ? 24 : 0)).zeroRowInsets() - .id("1") } .padding(.horizontal, 15) .rotationEffect(Angle(degrees: 180)) } - .introspectScrollView { scrollView in - scrollView.drawsBackground = false - - // Hide scrollbar - scrollView.scrollerInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: -20) - } .environment(\.defaultMinListRowHeight, 1) // By SwiftUI's logic, 0 is negative so we use 1 instead .background(.clear) .padding(.top, 74) // Ensure List doesn't go below text input field (and its border radius) + .introspectTableView { tableView in + tableView.enclosingScrollView!.drawsBackground = false +// tableView.enclosingScrollView!.rotate(byDegrees: 180) + + // Hide scrollbar + tableView.enclosingScrollView!.scrollerInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: -20) + } .rotationEffect(Angle(degrees: 180)) } } From 2fea2a1e6e417bb85fd8f225b031537bee0d47e9 Mon Sep 17 00:00:00 2001 From: 12944qwerty Date: Wed, 27 Sep 2023 10:16:19 -0400 Subject: [PATCH 14/14] fix click --- .../Message/MessageRenderViews/MessageView.swift | 14 +++++++++----- Swiftcord/Views/Message/MessagesView.swift | 3 ++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Swiftcord/Views/Message/MessageRenderViews/MessageView.swift b/Swiftcord/Views/Message/MessageRenderViews/MessageView.swift index a452c7e7..73fe42c9 100644 --- a/Swiftcord/Views/Message/MessageRenderViews/MessageView.swift +++ b/Swiftcord/Views/Message/MessageRenderViews/MessageView.swift @@ -65,11 +65,14 @@ struct MessageView: View, Equatable { VStack(alignment: .leading, spacing: 6) { // This message is a reply! if message.type == .reply { - ReferenceMessageView(referencedMsg: message.referenced_message).onTapGesture { - if let referencedID = message.referenced_message?.id { - onQuoteClick(referencedID) - } - } + Button { + if let referencedID = message.referenced_message?.id { + onQuoteClick(referencedID) + } + } label: { + ReferenceMessageView(referencedMsg: message.referenced_message) + } + .buttonStyle(.borderless) } HStack( alignment: MessageView.defaultTypes.contains(message.type) ? .top : .center, @@ -119,6 +122,7 @@ struct MessageView: View, Equatable { } .padding(.trailing, 32) .padding(.vertical, Self.lineSpacing / 2) + .padding(.horizontal, 15) .background( Rectangle() .fill(.blue) diff --git a/Swiftcord/Views/Message/MessagesView.swift b/Swiftcord/Views/Message/MessagesView.swift index 0c0b9eef..44a12635 100644 --- a/Swiftcord/Views/Message/MessagesView.swift +++ b/Swiftcord/Views/Message/MessagesView.swift @@ -264,6 +264,7 @@ struct MessagesView: View { MessagesViewHeader(chl: ctx.channel) .zeroRowInsets() .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 15) } else { loadingSkeleton .zeroRowInsets() @@ -274,9 +275,9 @@ struct MessagesView: View { viewModel.fetchMessagesTask = nil } } + .padding(.horizontal, 15) } } - .padding(.horizontal, 15) .rotationEffect(Angle(degrees: 180)) } .environment(\.defaultMinListRowHeight, 1) // By SwiftUI's logic, 0 is negative so we use 1 instead