Various optimizations to views & images rendering

This commit is contained in:
Thomas Ricouard 2023-02-17 18:17:51 +01:00
parent 881816730c
commit f09781582f
18 changed files with 125 additions and 74 deletions

View file

@ -135,9 +135,7 @@ struct IceCubesApp: App {
userPreferences.showiPadSecondaryColumn
{
Divider().edgesIgnoringSafeArea(.all)
NotificationsTab(popToRootTab: $popToRootTab, lockedType: nil)
.environment(\.isSecondaryColumn, true)
.frame(maxWidth: 360)
notificationsSecondaryColumn
}
}
}
@ -145,6 +143,13 @@ struct IceCubesApp: App {
sideBarLoadedTabs.removeAll()
}
}
private var notificationsSecondaryColumn: some View {
NotificationsTab(popToRootTab: $popToRootTab, lockedType: nil)
.environment(\.isSecondaryColumn, true)
.frame(maxWidth: 360)
.id(appAccountsManager.currentAccount.id)
}
private var tabBarView: some View {
TabView(selection: .init(get: {

View file

@ -55,9 +55,10 @@ public struct AvatarView: View {
.frame(width: size.size.width, height: size.size.height)
} else {
LazyImage(url: url) { state in
if let image = state.image {
image
.resizingMode(.aspectFit)
if let image = state.imageContainer?.image {
SwiftUI.Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
} else {
placeholderView
}

View file

@ -5,9 +5,27 @@ private struct SecondaryColumnKey: EnvironmentKey {
static let defaultValue = false
}
private struct ExtraLeadingInset: EnvironmentKey {
static let defaultValue: CGFloat = 0
}
private struct IsCompact: EnvironmentKey {
static let defaultValue: Bool = false
}
public extension EnvironmentValues {
var isSecondaryColumn: Bool {
get { self[SecondaryColumnKey.self] }
set { self[SecondaryColumnKey.self] = newValue }
}
var extraLeadingInset: CGFloat {
get { self[ExtraLeadingInset.self] }
set { self[ExtraLeadingInset.self] = newValue }
}
var isCompact: Bool {
get { self[IsCompact.self] }
set { self[IsCompact.self] = newValue }
}
}

View file

@ -81,7 +81,7 @@ public struct ExploreView: View {
private var loadingView: some View {
ForEach(Status.placeholders()) { status in
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath, isCompact: false))
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
.padding(.vertical, 8)
.redacted(reason: .placeholder)
.listRowBackground(theme.primaryBackgroundColor)
@ -184,7 +184,7 @@ public struct ExploreView: View {
Section("explore.section.trending.posts") {
ForEach(viewModel.trendingStatuses
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count)) { status in
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath, isCompact: false))
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
.listRowBackground(theme.primaryBackgroundColor)
.padding(.vertical, 8)
}
@ -192,7 +192,7 @@ public struct ExploreView: View {
NavigationLink {
List {
ForEach(viewModel.trendingStatuses) { status in
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath, isCompact: false))
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
.listRowBackground(theme.primaryBackgroundColor)
.padding(.vertical, 8)
}

View file

@ -6,24 +6,31 @@
//
import Models
import Foundation
extension Array where Element == Notification {
func consolidated(selectedType: Notification.NotificationType?) -> [ConsolidatedNotification] {
Dictionary(grouping: self) { $0.consolidationId(selectedType: selectedType) }
.values
.compactMap { notifications in
guard let notification = notifications.first,
let supportedType = notification.supportedType
else { return nil }
extension Array where Element == Models.Notification {
func consolidated(selectedType: Models.Notification.NotificationType?) async -> [ConsolidatedNotification] {
await withCheckedContinuation({ result in
DispatchQueue.global().async {
let notifications: [ConsolidatedNotification] =
Dictionary(grouping: self) { $0.consolidationId(selectedType: selectedType) }
.values
.compactMap { notifications in
guard let notification = notifications.first,
let supportedType = notification.supportedType
else { return nil }
return ConsolidatedNotification(notifications: notifications,
type: supportedType,
createdAt: notification.createdAt,
accounts: notifications.map(\.account),
status: notification.status)
}
.sorted {
$0.createdAt.asDate > $1.createdAt.asDate
return ConsolidatedNotification(notifications: notifications,
type: supportedType,
createdAt: notification.createdAt,
accounts: notifications.map(\.account),
status: notification.status)
}
.sorted {
$0.createdAt.asDate > $1.createdAt.asDate
}
result.resume(returning: notifications)
}
})
}
}

View file

@ -7,13 +7,13 @@ import SwiftUI
import Network
struct NotificationRowView: View {
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var theme: Theme
@Environment(\.redactionReasons) private var reasons
let notification: ConsolidatedNotification
let client: Client
let routerPath: RouterPath
let followRequests: [Account]
var body: some View {
HStack(alignment: .top, spacing: 8) {
@ -28,8 +28,7 @@ struct NotificationRowView: View {
makeMainLabel(type: notification.type)
makeContent(type: notification.type)
if notification.type == .follow_request,
currentAccount.followRequests.map(\.id).contains(notification.accounts[0].id)
{
followRequests.map(\.id).contains(notification.accounts[0].id) {
FollowRequestButtons(account: notification.accounts[0])
}
}
@ -137,19 +136,18 @@ struct NotificationRowView: View {
StatusRowView(viewModel: .init(status: status,
client: client,
routerPath: routerPath,
isCompact: true,
showActions: true))
} else {
StatusRowView(viewModel: .init(status: status,
client: client,
routerPath: routerPath,
isCompact: true,
showActions: false))
.lineLimit(4)
.foregroundColor(.gray)
}
Spacer()
}
.environment(\.isCompact, true)
} else {
Group {
Text("@\(notification.accounts[0].acct)")

View file

@ -84,7 +84,10 @@ public struct NotificationsListView: View {
switch viewModel.state {
case .loading:
ForEach(ConsolidatedNotification.placeholders()) { notification in
NotificationRowView(notification: notification, client: client, routerPath: routerPath)
NotificationRowView(notification: notification,
client: client,
routerPath: routerPath,
followRequests: account.followRequests)
.redacted(reason: .placeholder)
.listRowInsets(.init(top: 12,
leading: .layoutPadding + 4,
@ -103,13 +106,17 @@ public struct NotificationsListView: View {
.listSectionSeparator(.hidden)
} else {
ForEach(notifications) { notification in
NotificationRowView(notification: notification, client: client, routerPath: routerPath)
NotificationRowView(notification: notification,
client: client,
routerPath: routerPath,
followRequests: account.followRequests)
.listRowInsets(.init(top: 12,
leading: .layoutPadding + 4,
bottom: 12,
trailing: .layoutPadding))
.listRowBackground(notification.type == .mention && lockedType != .mention ?
theme.secondaryBackgroundColor : theme.primaryBackgroundColor)
.id(notification.id)
}
}

View file

@ -69,7 +69,7 @@ class NotificationsViewModel: ObservableObject {
maxId: nil,
types: queryTypes,
limit: Constants.notificationLimit))
consolidatedNotifications = notifications.consolidated(selectedType: selectedType)
consolidatedNotifications = await notifications.consolidated(selectedType: selectedType)
nextPageState = notifications.count < Constants.notificationLimit ? .none : .hasNextPage
} else if let firstId = consolidatedNotifications.first?.id {
var newNotifications: [Models.Notification] = await fetchNewPages(minId: firstId, maxPages: 10)
@ -77,13 +77,15 @@ class NotificationsViewModel: ObservableObject {
newNotifications = newNotifications.filter { notification in
!consolidatedNotifications.contains(where: { $0.id == notification.id })
}
consolidatedNotifications.insert(
await consolidatedNotifications.insert(
contentsOf: newNotifications.consolidated(selectedType: selectedType),
at: 0
)
}
await currentAccount.fetchFollowerRequests()
if consolidatedNotifications.contains(where: { $0.type == .follow_request }) {
await currentAccount.fetchFollowerRequests()
}
withAnimation {
state = .display(notifications: consolidatedNotifications,
@ -129,8 +131,10 @@ class NotificationsViewModel: ObservableObject {
maxId: lastId,
types: queryTypes,
limit: Constants.notificationLimit))
consolidatedNotifications.append(contentsOf: newNotifications.consolidated(selectedType: selectedType))
await currentAccount?.fetchFollowerRequests()
await consolidatedNotifications.append(contentsOf: newNotifications.consolidated(selectedType: selectedType))
if consolidatedNotifications.contains(where: { $0.type == .follow_request }) {
await currentAccount?.fetchFollowerRequests()
}
state = .display(notifications: consolidatedNotifications,
nextPageState: newNotifications.count < Constants.notificationLimit ? .none : .hasNextPage)
} catch {
@ -159,14 +163,14 @@ class NotificationsViewModel: ObservableObject {
{
// If the notification type can be consolidated, try to consolidate with the latest row
let latestConsolidatedNotification = consolidatedNotifications.removeFirst()
consolidatedNotifications.insert(
await consolidatedNotifications.insert(
contentsOf: ([event.notification] + latestConsolidatedNotification.notifications)
.consolidated(selectedType: selectedType),
at: 0
)
} else {
// Otherwise, just insert the new notification
consolidatedNotifications.insert(
await consolidatedNotifications.insert(
contentsOf: [event.notification].consolidated(selectedType: selectedType),
at: 0
)

View file

@ -104,18 +104,20 @@ public struct StatusDetailView: View {
}
let viewModel: StatusRowViewModel = .init(status: status,
client: client,
routerPath: routerPath,
isCompact: false)
return HStack {
routerPath: routerPath)
return HStack(spacing: 0) {
if isReplyToPrevious {
Rectangle()
.fill(theme.tintColor)
.frame(width: 2)
Spacer(minLength: 8)
}
if self.viewModel.statusId == status.id {
makeCurrentStatusView(status: status)
.environment(\.extraLeadingInset, isReplyToPrevious ? 10 : 0)
} else {
StatusRowView(viewModel: viewModel)
.environment(\.extraLeadingInset, isReplyToPrevious ? 10 : 0)
}
}
.listRowBackground(viewModel.highlightRowColor)
@ -130,7 +132,6 @@ public struct StatusDetailView: View {
StatusRowView(viewModel: .init(status: status,
client: client,
routerPath: routerPath,
isCompact: false,
isFocused: !viewModel.isLoadingContext))
.overlay {
GeometryReader { reader in
@ -157,7 +158,7 @@ public struct StatusDetailView: View {
private var loadingDetailView: some View {
ForEach(Status.placeholders()) { status in
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath, isCompact: false))
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
.redacted(reason: .placeholder)
}
}

View file

@ -5,7 +5,7 @@ import Models
import NukeUI
import SwiftUI
struct StatusEditorMediaView: View {
struct StatusEditorMediaView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var currentInstance: CurrentInstance
@ObservedObject var viewModel: StatusEditorViewModel

View file

@ -26,8 +26,8 @@ public struct StatusEmbeddedView: View {
StatusRowView(viewModel: .init(status: status,
client: client,
routerPath: routerPath,
isCompact: true,
showActions: false))
.environment(\.isCompact, true)
}
Spacer()
}

View file

@ -24,7 +24,7 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
switch fetcher.statusesState {
case .loading:
ForEach(Status.placeholders()) { status in
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath, isCompact: false))
StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath))
.redacted(reason: .placeholder)
}
case .error:
@ -40,7 +40,7 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
case let .display(statuses, nextPageState):
ForEach(statuses, id: \.viewId) { status in
let viewModel = StatusRowViewModel(status: status, client: client, routerPath: routerPath, isCompact: false, isRemote: isRemote)
let viewModel = StatusRowViewModel(status: status, client: client, routerPath: routerPath, isRemote: isRemote)
if viewModel.filter?.filter.filterAction != .hide {
StatusRowView(viewModel: viewModel)
.id(status.id)

View file

@ -54,6 +54,7 @@ struct VideoPlayerView: View {
}.onAppear {
viewModel.preparePlayer(autoPlay: preferences.autoPlayVideo)
}
.cornerRadius(4)
.onChange(of: scenePhase, perform: { scenePhase in
switch scenePhase {
case .background, .inactive:

View file

@ -8,6 +8,7 @@ import SwiftUI
public struct StatusRowView: View {
@Environment(\.redactionReasons) private var reasons
@Environment(\.isCompact) private var isCompact: Bool
@EnvironmentObject private var theme: Theme
@ -34,12 +35,12 @@ public struct StatusRowView: View {
} else {
let status: AnyStatus = viewModel.status.reblog ?? viewModel.status
VStack(alignment: .leading) {
if !viewModel.isCompact, theme.avatarPosition == .leading {
if !isCompact, theme.avatarPosition == .leading {
StatusRowReblogView(viewModel: viewModel)
StatusRowReplyView(viewModel: viewModel)
}
HStack(alignment: .top, spacing: .statusColumnsSpacing) {
if !viewModel.isCompact,
if !isCompact,
theme.avatarPosition == .leading {
Button {
viewModel.routerPath.navigate(to: .accountDetailWithAccount(account: status.account))
@ -48,13 +49,13 @@ public struct StatusRowView: View {
}
}
VStack(alignment: .leading) {
if !viewModel.isCompact, theme.avatarPosition == .top {
if !isCompact, theme.avatarPosition == .top {
StatusRowReblogView(viewModel: viewModel)
StatusRowReplyView(viewModel: viewModel)
}
VStack(alignment: .leading, spacing: 8) {
let status: AnyStatus = viewModel.status.reblog ?? viewModel.status
if !viewModel.isCompact {
if !isCompact {
StatusRowHeaderView(status: status, viewModel: viewModel)
}
StatusRowContentView(status: status, viewModel: viewModel)
@ -82,7 +83,7 @@ public struct StatusRowView: View {
.onAppear {
viewModel.markSeen()
if reasons.isEmpty {
if !viewModel.isCompact, viewModel.embeddedStatus == nil {
if !isCompact, viewModel.embeddedStatus == nil {
Task {
await viewModel.loadEmbeddedStatus()
}
@ -93,12 +94,12 @@ public struct StatusRowView: View {
contextMenu
}
.swipeActions(edge: .trailing) {
if !viewModel.isCompact {
if !isCompact {
StatusRowSwipeView(viewModel: viewModel, mode: .trailing)
}
}
.swipeActions(edge: .leading) {
if !viewModel.isCompact {
if !isCompact {
StatusRowSwipeView(viewModel: viewModel, mode: .leading)
}
}

View file

@ -9,7 +9,6 @@ import DesignSystem
@MainActor
public class StatusRowViewModel: ObservableObject {
let status: Status
let isCompact: Bool
let isFocused: Bool
let isRemote: Bool
let showActions: Bool
@ -61,7 +60,6 @@ public class StatusRowViewModel: ObservableObject {
public init(status: Status,
client: Client,
routerPath: RouterPath,
isCompact: Bool = false,
isFocused: Bool = false,
isRemote: Bool = false,
showActions: Bool = true)
@ -69,7 +67,6 @@ public class StatusRowViewModel: ObservableObject {
self.status = status
self.client = client
self.routerPath = routerPath
self.isCompact = isCompact
self.isFocused = isFocused
self.isRemote = isRemote
self.showActions = showActions

View file

@ -18,9 +18,12 @@ public struct StatusRowCardView: View {
VStack(alignment: .leading) {
if let imageURL = card.image {
LazyImage(url: imageURL) { state in
if let image = state.image {
image
.resizingMode(.aspectFill)
if let image = state.imageContainer?.image {
SwiftUI.Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(height: 200)
.clipped()
} else if state.isLoading {
Rectangle()
.fill(Color.gray)

View file

@ -5,6 +5,8 @@ import Env
struct StatusRowContentView: View {
@Environment(\.redactionReasons) private var reasons
@Environment(\.isCompact) private var isCompact
@EnvironmentObject private var theme: Theme
let status: AnyStatus
@ -23,7 +25,7 @@ struct StatusRowContentView: View {
}
if !reasons.contains(.placeholder),
!viewModel.isCompact,
!isCompact,
(viewModel.isEmbedLoading || viewModel.embeddedStatus != nil) {
StatusEmbeddedView(status: viewModel.embeddedStatus ?? Status.placeholder(),
client: viewModel.client,
@ -31,13 +33,14 @@ struct StatusRowContentView: View {
.fixedSize(horizontal: false, vertical: true)
.redacted(reason: viewModel.isEmbedLoading ? .placeholder : [])
.shimmering(active: viewModel.isEmbedLoading)
.transition(.opacity)
}
if !status.mediaAttachments.isEmpty {
HStack {
StatusRowMediaPreviewView(attachments: status.mediaAttachments,
sensitive: status.sensitive,
isNotifications: viewModel.isCompact)
isNotifications: isCompact)
if theme.statusDisplayStyle == .compact {
Spacer()
}
@ -47,7 +50,7 @@ struct StatusRowContentView: View {
if let card = status.card,
!viewModel.isEmbedLoading,
!viewModel.isCompact,
!isCompact,
theme.statusDisplayStyle == .large,
status.content.statusesURLs.isEmpty,
status.mediaAttachments.isEmpty

View file

@ -8,6 +8,7 @@ import SwiftUI
public struct StatusRowMediaPreviewView: View {
@Environment(\.openURL) private var openURL
@Environment(\.isSecondaryColumn) private var isSecondaryColumn
@Environment(\.extraLeadingInset) private var extraLeadingInset: CGFloat
@EnvironmentObject var sceneDelegate: SceneDelegate
@EnvironmentObject private var preferences: UserPreferences
@ -37,7 +38,7 @@ public struct StatusRowMediaPreviewView: View {
if UIDevice.current.userInterfaceIdiom == .pad && sceneDelegate.windowWidth < (.maxColumnWidth + .sidebarWidth) {
sidebarWidth = .sidebarWidth
}
return (.layoutPadding * 2) + avatarColumnWidth + sidebarWidth
return (.layoutPadding * 2) + avatarColumnWidth + sidebarWidth + extraLeadingInset
}
private var imageMaxHeight: CGFloat {
@ -152,11 +153,13 @@ public struct StatusRowMediaPreviewView: View {
switch attachment.supportedType {
case .image:
LazyImage(url: attachment.url) { state in
if let image = state.image {
image
.resizingMode(.aspectFill)
.cornerRadius(4)
if let image = state.imageContainer?.image {
SwiftUI.Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: newSize.width, height: newSize.height)
.clipped()
.cornerRadius(4)
} else {
RoundedRectangle(cornerRadius: 4)
.fill(Color.gray)
@ -204,9 +207,13 @@ public struct StatusRowMediaPreviewView: View {
case .image:
ZStack(alignment: .bottomTrailing) {
LazyImage(url: attachment.url) { state in
if let image = state.image {
image
.resizingMode(.aspectFill)
if let image = state.imageContainer?.image {
SwiftUI.Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width)
.frame(maxHeight: imageMaxHeight)
.clipped()
.cornerRadius(4)
} else if state.isLoading {
RoundedRectangle(cornerRadius: 4)
@ -215,8 +222,6 @@ public struct StatusRowMediaPreviewView: View {
.frame(maxWidth: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width)
}
}
.frame(maxWidth: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width)
.frame(height: imageMaxHeight)
if sensitive {
cornerSensitiveButton
}