Merge branch 'main' into tip-system

This commit is contained in:
Thomas Ricouard 2024-07-30 09:24:51 +02:00
commit 7b9544d1e0
22 changed files with 281 additions and 131 deletions

View file

@ -1514,7 +1514,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIconAlternate0 AppIconAlternate6 AppIconAlternate7 AppIconAlternate8 AppIconAlternate10 AppIconAlternate11 AppIconAlternate12 AppIconAlternate13 AppIconAlternate14 AppIconAlternate15 AppIconAlternate16 AppIconAlternate17 AppIconAlternate19 AppIconAlternate18 AppIconAlternate20 AppIconAlternate21 AppIconAlternate22 AppIconAlternate23 AppIconAlternate24 AppIconAlternate25 AppIconAlternate26 AppIconAlternate27 AppIconAlternate28 AppIconAlternate29 AppIconAlternate30 AppIconAlternate31 AppIconAlternate32 AppIconAlternate33 AppIconAlternate34 AppIconAlternate35 AppIconAlternate36 AppIconAlternate37 AppIconAlternate38 AppIconAlternate39 AppIconAlternate40 AppIconAlternate42 AppIconAlternate2 AppIconAlternate41 AppIconAlternate45 AppIconAlternate44 AppIconAlternate1 AppIconAlternate4 AppIconAlternate3 AppIconAlternate5 AppIconAlternate46 AppIconAlternate9 AppIconAlternate43";
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIconAlternate0 AppIconAlternate6 AppIconAlternate7 AppIconAlternate8 AppIconAlternate10 AppIconAlternate11 AppIconAlternate12 AppIconAlternate13 AppIconAlternate14 AppIconAlternate15 AppIconAlternate16 AppIconAlternate17 AppIconAlternate19 AppIconAlternate18 AppIconAlternate20 AppIconAlternate21 AppIconAlternate22 AppIconAlternate23 AppIconAlternate24 AppIconAlternate25 AppIconAlternate26 AppIconAlternate27 AppIconAlternate28 AppIconAlternate29 AppIconAlternate30 AppIconAlternate31 AppIconAlternate32 AppIconAlternate33 AppIconAlternate34 AppIconAlternate35 AppIconAlternate36 AppIconAlternate37 AppIconAlternate38 AppIconAlternate39 AppIconAlternate40 AppIconAlternate42 AppIconAlternate2 AppIconAlternate41 AppIconAlternate45 AppIconAlternate44 AppIconAlternate1 AppIconAlternate4 AppIconAlternate3 AppIconAlternate5 AppIconAlternate46 AppIconAlternate9 AppIconAlternate49 AppIconAlternate48 AppIconAlternate47 AppIconAlternate43";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
@ -1570,7 +1570,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIconAlternate0 AppIconAlternate6 AppIconAlternate7 AppIconAlternate8 AppIconAlternate10 AppIconAlternate11 AppIconAlternate12 AppIconAlternate13 AppIconAlternate14 AppIconAlternate15 AppIconAlternate16 AppIconAlternate17 AppIconAlternate19 AppIconAlternate18 AppIconAlternate20 AppIconAlternate21 AppIconAlternate22 AppIconAlternate23 AppIconAlternate24 AppIconAlternate25 AppIconAlternate26 AppIconAlternate27 AppIconAlternate28 AppIconAlternate29 AppIconAlternate30 AppIconAlternate31 AppIconAlternate32 AppIconAlternate33 AppIconAlternate34 AppIconAlternate35 AppIconAlternate36 AppIconAlternate37 AppIconAlternate38 AppIconAlternate39 AppIconAlternate40 AppIconAlternate42 AppIconAlternate2 AppIconAlternate41 AppIconAlternate45 AppIconAlternate44 AppIconAlternate1 AppIconAlternate4 AppIconAlternate3 AppIconAlternate5 AppIconAlternate46 AppIconAlternate9 AppIconAlternate43";
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIconAlternate0 AppIconAlternate6 AppIconAlternate7 AppIconAlternate8 AppIconAlternate10 AppIconAlternate11 AppIconAlternate12 AppIconAlternate13 AppIconAlternate14 AppIconAlternate15 AppIconAlternate16 AppIconAlternate17 AppIconAlternate19 AppIconAlternate18 AppIconAlternate20 AppIconAlternate21 AppIconAlternate22 AppIconAlternate23 AppIconAlternate24 AppIconAlternate25 AppIconAlternate26 AppIconAlternate27 AppIconAlternate28 AppIconAlternate29 AppIconAlternate30 AppIconAlternate31 AppIconAlternate32 AppIconAlternate33 AppIconAlternate34 AppIconAlternate35 AppIconAlternate36 AppIconAlternate37 AppIconAlternate38 AppIconAlternate39 AppIconAlternate40 AppIconAlternate42 AppIconAlternate2 AppIconAlternate41 AppIconAlternate45 AppIconAlternate44 AppIconAlternate1 AppIconAlternate4 AppIconAlternate3 AppIconAlternate5 AppIconAlternate46 AppIconAlternate9 AppIconAlternate49 AppIconAlternate48 AppIconAlternate47 AppIconAlternate43";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;

View file

@ -122,6 +122,7 @@ struct AppView: View {
.tag(tab)
}
}
.id(availableTabs.count) /// Resets the TabView state when the number of tabs changes to avoid navigation bar issues and prevent crashes
.introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in
tabview.tabBar.isHidden = horizontalSizeClass == .regular
tabview.customizableViewControllers = []

View file

@ -136,11 +136,12 @@ struct AddAccountView: View {
instance = nil
instanceFetchError = nil
}
} catch _ as DecodingError {
} catch _ as ServerError {
instance = nil
instanceFetchError = "account.add.error.instance-not-supported"
} catch {
instance = nil
instanceFetchError = nil
}
}
}

View file

@ -27,6 +27,8 @@ struct IconSelectorView: View {
case alt39, alt40, alt41, alt42, alt43
case alt44, alt45
case alt46
case alt47, alt48
case alt49
var appIconName: String {
return "AppIconAlternate\(rawValue)"
@ -51,6 +53,8 @@ struct IconSelectorView: View {
IconSelector(title: "\("settings.app.icon.designed-by".localized) Duncan Horne", icons: [.alt38]),
IconSelector(title: "\("settings.app.icon.designed-by".localized) BeAware@social.beaware.live", icons: [.alt39, .alt40, .alt41, .alt42, .alt43]),
IconSelector(title: "\("settings.app.icon.designed-by".localized) Simone Margio", icons: [.alt44, .alt45]),
IconSelector(title: "\("settings.app.icon.designed-by".localized) Peter Broqvist (@PKB)", icons: [.alt47, .alt48]),
IconSelector(title: "\("settings.app.icon.designed-by".localized) Oz Tsori (@oztsori)", icons: [.alt49]),
]
}

View file

@ -15,17 +15,19 @@ struct ToolbarTab: ToolbarContent {
var body: some ToolbarContent {
if !isSecondaryColumn {
ToolbarItem(placement: .topBarLeading) {
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
Button {
withAnimation {
userPreferences.isSidebarExpanded.toggle()
}
} label: {
if userPreferences.isSidebarExpanded {
Image(systemName: "sidebar.squares.left")
} else {
Image(systemName: "sidebar.left")
if horizontalSizeClass == .regular {
ToolbarItem(placement: .topBarLeading) {
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
Button {
withAnimation {
userPreferences.isSidebarExpanded.toggle()
}
} label: {
if userPreferences.isSidebarExpanded {
Image(systemName: "sidebar.squares.left")
} else {
Image(systemName: "sidebar.left")
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

View file

@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "AppIconAlternate47-fs8.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

View file

@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "AppIconAlternate48-fs8.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "AppIconAlternate49-fs8.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -710,6 +710,12 @@
"%@ ist keine valide Instanz" : {
"extractionState" : "stale",
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ no es una instancia válida"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
@ -13521,6 +13527,12 @@
"value" : "Delete avatar"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Eliminar imagen de perfil"
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
@ -13539,6 +13551,12 @@
"value" : "Verwijder profielfoto"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Avatarı Sil"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
@ -14032,6 +14050,12 @@
"value" : "Delete header"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Eliminar cabecera"
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
@ -14050,6 +14074,12 @@
"value" : "Verwijder omslagfoto"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Başlığı Sil"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
@ -28582,8 +28612,8 @@
},
"es" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Enlaces que son tendencia"
"state" : "translated",
"value" : "Noticias"
}
},
"eu" : {
@ -28642,8 +28672,8 @@
},
"tr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Yükselişteki Bağlantılar"
"state" : "translated",
"value" : "Trend Bağlantılar"
}
},
"uk" : {
@ -28762,7 +28792,7 @@
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Yükselişteki Gönderiler"
"value" : "Trend Gönderiler"
}
},
"uk" : {
@ -28881,7 +28911,7 @@
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Yükselişteki Etiketler"
"value" : "Trend Etiketler"
}
},
"uk" : {
@ -35814,7 +35844,7 @@
},
"es" : {
"stringUnit" : {
"state" : "needs_review",
"state" : "translated",
"value" : "Ajustes ..."
}
},
@ -35874,7 +35904,7 @@
},
"tr" : {
"stringUnit" : {
"state" : "needs_review",
"state" : "translated",
"value" : "Ayarlar ..."
}
},
@ -41454,6 +41484,12 @@
"value" : "Einen Status veröffentlichen"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hacer una publicación"
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
@ -41633,6 +41669,12 @@
"value" : "Bilder veröffentlichen"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Publicar imágenes"
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
@ -41674,6 +41716,12 @@
"value" : "Status auf Mastodon veröffentlichen"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hacer una publicación en Mastodon"
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
@ -62385,7 +62433,26 @@
}
},
"Show Content Gradient" : {
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Mostrar contenido con gradiente"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "İçerik Gradyanını Göster"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "启用渐变嘟文背景"
}
}
}
},
"status.action.bookmark" : {
"extractionState" : "manual",
@ -78056,7 +78123,7 @@
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Yükselişte"
"value" : "Trend"
}
},
"uk" : {
@ -82904,14 +82971,14 @@
"plural" : {
"one" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "%lld persona hablando"
"state" : "translated",
"value" : "%lld publicación"
}
},
"other" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "%lld personas hablando"
"state" : "translated",
"value" : "%lld publicaciones"
}
}
}
@ -83096,14 +83163,14 @@
"plural" : {
"one" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "%lld kişi konuşuyor"
"state" : "translated",
"value" : "%lld gönderi"
}
},
"other" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "%lld kişi konuşuyor"
"state" : "translated",
"value" : "%lld gönderi"
}
}
}
@ -83132,13 +83199,13 @@
"plural" : {
"one" : {
"stringUnit" : {
"state" : "needs_review",
"state" : "translated",
"value" : "%lld 人正在参与讨论"
}
},
"other" : {
"stringUnit" : {
"state" : "needs_review",
"state" : "translated",
"value" : "%lld 人正在参与讨论"
}
}
@ -83254,6 +83321,12 @@
"value" : "Ice Cubes benutzen, um einen Status auf Mastodon zu veröffentlichen"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Usar Ice Cubes para hacer una publicación en Mastodon"
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
@ -83295,6 +83368,12 @@
"value" : "Ice Cubes benutzen, um einen Status mit einem Bild auf Mastodon zu veröffentlichen"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Usar Ice Cubes para hacer una publicación con una imagen en Mastodon"
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
@ -83421,4 +83500,4 @@
}
},
"version" : "1.0"
}
}

View file

@ -9,7 +9,7 @@ public let availableColorsSets: [ColorSetCouple] =
.init(light: ConstellationLight(), dark: ConstellationDark()),
.init(light: ThreadsLight(), dark: ThreadsDark())]
public protocol ColorSet {
public protocol ColorSet: Sendable {
var name: ColorSetName { get }
var scheme: ColorScheme { get }
var tintColor: Color { get set }
@ -18,11 +18,11 @@ public protocol ColorSet {
var labelColor: Color { get set }
}
public enum ColorScheme: String {
public enum ColorScheme: String, Sendable {
case dark, light
}
public enum ColorSetName: String {
public enum ColorSetName: String, Sendable {
case iceCubeDark = "Ice Cube - Dark"
case iceCubeLight = "Ice Cube - Light"
case iceCubeNeonDark = "Ice Cube Neon - Dark"
@ -39,7 +39,7 @@ public enum ColorSetName: String {
case threadsDark = "Threads - Dark"
}
public struct ColorSetCouple: Identifiable {
public struct ColorSetCouple: Identifiable, Sendable {
public var id: String {
dark.name.rawValue + light.name.rawValue
}

View file

@ -22,6 +22,23 @@ struct MediaUIAttachmentImageView: View {
.progressViewStyle(.circular)
}
}
.draggable(MediaUIImageTransferable(url: url))
.contextMenu {
MediaUIShareLink(url: url, type: .image)
Button {
Task {
let transferable = MediaUIImageTransferable(url: url)
UIPasteboard.general.image = UIImage(data: await transferable.fetchData())
}
} label: {
Label("status.media.contextmenu.copy", systemImage: "doc.on.doc")
}
Button {
UIPasteboard.general.url = url
} label: {
Label("status.action.copy-link", systemImage: "link")
}
}
}
}
}

View file

@ -0,0 +1,16 @@
import SwiftUI
struct MediaUIShareLink: View, @unchecked Sendable {
let url: URL
let type: DisplayType
var body: some View {
if type == .image {
let transferable = MediaUIImageTransferable(url: url)
ShareLink(item: transferable, preview: .init("status.media.contextmenu.share",
image: transferable))
} else {
ShareLink(item: url)
}
}
}

View file

@ -6,13 +6,7 @@ struct ShareToolbarItem: ToolbarContent, @unchecked Sendable {
var body: some ToolbarContent {
ToolbarItem(placement: .topBarTrailing) {
if type == .image {
let transferable = MediaUIImageTransferable(url: url)
ShareLink(item: transferable, preview: .init("status.media.contextmenu.share",
image: transferable))
} else {
ShareLink(item: url)
}
MediaUIShareLink(url: url, type: type)
}
}
}

View file

@ -19,7 +19,6 @@ public extension StatusEditor {
didSet {
if let itemsProvider {
mediaContainers = []
processItemsProvider(items: itemsProvider)
}
}
}

View file

@ -124,6 +124,7 @@ public struct StatusRowView: View {
}
}
}
.if(viewModel.url != nil) { $0.draggable(viewModel.url!) }
.contextMenu {
contextMenu
.onAppear {

View file

@ -105,7 +105,11 @@ import SwiftUI
status.reblog?.inReplyToId != nil || status.reblog?.inReplyToAccountId != nil ||
status.inReplyToId != nil || status.inReplyToAccountId != nil
}
var url: URL? {
(status.reblog?.url ?? status.url).flatMap(URL.init(string:))
}
@ViewBuilder
func makeBackgroundColor(isHomeTimeline: Bool) -> some View {
if isHomeTimeline, theme.showContentGradient {

View file

@ -81,6 +81,7 @@ public struct StatusRowCardView: View {
.stroke(.gray.opacity(0.35), lineWidth: 1)
}
}
.draggable(url)
.contextMenu {
ShareLink(item: url) {
Label("status.card.share", systemImage: "square.and.arrow.up")

View file

@ -88,9 +88,7 @@ struct StatusRowContextMenu: View {
Divider()
Menu("status.action.share-title") {
if let urlString = viewModel.status.reblog?.url ?? viewModel.status.url,
let url = URL(string: urlString)
{
if let url = viewModel.url {
ShareLink(item: url,
subject: Text(viewModel.status.reblog?.account.safeDisplayName ?? viewModel.status.account.safeDisplayName),
message: Text(viewModel.status.reblog?.content.asRawText ?? viewModel.status.content.asRawText))
@ -133,7 +131,7 @@ struct StatusRowContextMenu: View {
}
}
if let url = URL(string: viewModel.status.reblog?.url ?? viewModel.status.url ?? "") {
if let url = viewModel.url {
Button { UIApplication.shared.open(url) } label: {
Label("status.action.view-in-browser", systemImage: "safari")
}
@ -152,7 +150,7 @@ struct StatusRowContextMenu: View {
}
Button {
UIPasteboard.general.string = viewModel.status.reblog?.url ?? viewModel.status.url
UIPasteboard.general.url = viewModel.url
} label: {
Label("status.action.copy-link", systemImage: "link")
}

View file

@ -307,109 +307,100 @@ extension TimelineViewModel: StatusesFetcher {
private func fetchNewPagesFrom(latestStatus: String, client: Client) async throws {
canStreamEvents = false
let initialTimeline = timeline
var newStatuses: [Status] = await fetchNewPages(minId: latestStatus, maxPages: 5)
// Dedup statuses, a status with the same id could have been streamed in.
let newStatuses = await fetchAndDedupNewStatuses(latestStatus: latestStatus, client: client)
guard !newStatuses.isEmpty,
isTimelineVisible,
!Task.isCancelled,
initialTimeline == timeline else {
canStreamEvents = true
return
}
await updateTimelineWithNewStatuses(newStatuses)
if !Task.isCancelled, let latest = await datasource.get().first {
pendingStatusesObserver.isLoadingNewStatuses = true
try await fetchNewPagesFrom(latestStatus: latest.id, client: client)
}
}
private func fetchAndDedupNewStatuses(latestStatus: String, client: Client) async -> [Status] {
var newStatuses = await fetchNewPages(minId: latestStatus, maxPages: 5)
let ids = await datasource.get().map(\.id)
newStatuses = newStatuses.filter { status in
!ids.contains(where: { $0 == status.id })
}
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
// If no new statuses, resume streaming and exit.
guard !newStatuses.isEmpty else {
canStreamEvents = true
return
}
// If the timeline is not visible, we don't update it as it would mess up the user position.
guard isTimelineVisible else {
canStreamEvents = true
return
}
// Return if task has been cancelled.
guard !Task.isCancelled else {
canStreamEvents = true
return
}
// As this is a long runnign task we need to ensure that the user didn't changed the timeline filter.
guard initialTimeline == timeline else {
canStreamEvents = true
return
}
// Keep track of the top most status, so we can scroll back to it after view update.
return newStatuses
}
private func updateTimelineWithNewStatuses(_ newStatuses: [Status]) async {
let topStatus = await datasource.getFiltered().first
// Insert new statuses in internal datasource.
await datasource.insert(contentOf: newStatuses, at: 0)
// Cache statuses for timeline.
await cache()
// Append new statuses in the timeline indicator.
pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map(\.id), at: 0)
// High chance the user is scrolled to the top.
// We need to update the statuses state, and then scroll to the previous top most status.
if let topStatus, visibileStatuses.contains(where: { $0.id == topStatus.id }), scrollToTopVisible {
pendingStatusesObserver.disableUpdate = true
let statuses = await datasource.getFiltered()
statusesState = .display(statuses: statuses,
nextPageState: statuses.count < 20 ? .none : .hasNextPage)
scrollToIndexAnimated = false
scrollToIndex = newStatuses.count + 1
DispatchQueue.main.async {
self.pendingStatusesObserver.disableUpdate = false
self.canStreamEvents = true
}
let statuses = await datasource.getFiltered()
let nextPageState: StatusesState.PagingState = statuses.count < 20 ? .none : .hasNextPage
if let topStatus = topStatus,
visibileStatuses.contains(where: { $0.id == topStatus.id }),
scrollToTopVisible {
updateTimelineWithScrollToTop(newStatuses: newStatuses, statuses: statuses, nextPageState: nextPageState)
} else {
// This will keep the scroll position (if the list is scrolled) and prepend statuses on the top.
let statuses = await datasource.getFiltered()
withAnimation {
statusesState = .display(statuses: statuses,
nextPageState: statuses.count < 20 ? .none : .hasNextPage)
canStreamEvents = true
}
updateTimelineWithAnimation(statuses: statuses, nextPageState: nextPageState)
}
if !Task.isCancelled,
let latest = await datasource.get().first
{
pendingStatusesObserver.isLoadingNewStatuses = true
try await fetchNewPagesFrom(latestStatus: latest.id, client: client)
}
// Refresh the timeline while keeping the scroll position to the top status.
private func updateTimelineWithScrollToTop(newStatuses: [Status], statuses: [Status], nextPageState: StatusesState.PagingState) {
pendingStatusesObserver.disableUpdate = true
statusesState = .display(statuses: statuses, nextPageState: nextPageState)
scrollToIndexAnimated = false
scrollToIndex = newStatuses.count + 1
DispatchQueue.main.async { [weak self] in
self?.pendingStatusesObserver.disableUpdate = false
self?.canStreamEvents = true
}
}
// Refresh the timeline while keeping the user current position.
// It works because a side effect of withAnimation is that it keep scroll position IF the List is not scrolled to the top.
private func updateTimelineWithAnimation(statuses: [Status], nextPageState: StatusesState.PagingState) {
withAnimation {
statusesState = .display(statuses: statuses, nextPageState: nextPageState)
canStreamEvents = true
}
}
private func fetchNewPages(minId: String, maxPages: Int) async -> [Status] {
guard let client else { return [] }
var pagesLoaded = 0
var allStatuses: [Status] = []
var latestMinId = minId
do {
while
!Task.isCancelled,
let newStatuses: [Status] =
try await client.get(endpoint: timeline.endpoint(sinceId: nil,
maxId: nil,
minId: latestMinId,
offset: datasource.get().count)),
!newStatuses.isEmpty,
pagesLoaded < maxPages
{
pagesLoaded += 1
for _ in 1...maxPages {
if Task.isCancelled { break }
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(
sinceId: nil,
maxId: nil,
minId: latestMinId,
offset: nil
))
if newStatuses.isEmpty { break }
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
allStatuses.insert(contentsOf: newStatuses, at: 0)
latestMinId = newStatuses.first?.id ?? ""
latestMinId = newStatuses.first?.id ?? latestMinId
}
} catch {
return allStatuses
}
return allStatuses
}