Merge branch 'main' into iOS-18

This commit is contained in:
Thomas Ricouard 2024-07-21 11:20:44 +02:00
commit 57452a676a
9 changed files with 117 additions and 80 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;
@ -1581,7 +1581,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

@ -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]),
]
}

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

@ -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
}