Improve new statuses view and animations.

This commit is contained in:
Marcin Czachursk 2023-02-08 20:18:03 +01:00
parent 23fd00490f
commit 13c9b52bc9
4 changed files with 114 additions and 40 deletions

View File

@ -15,7 +15,7 @@ public class ApplicationState: ObservableObject {
@Published var account: AccountModel?
@Published var lastSeenStatusId: String?
@Published var lastBackgroundRefresh: Date?
@Published var amountOfNewStatuses = 0
@Published var tintColor = TintColor.accentColor2
@Published var theme = Theme.system
@Published var avatarShape = AvatarShape.circle

View File

@ -21,6 +21,8 @@ struct VernissageApp: App {
@State var applicationViewMode: ApplicationViewMode = .loading
@State var tintColor = ApplicationState.shared.tintColor.color()
@State var theme = ApplicationState.shared.theme.colorScheme()
let timer = Timer.publish(every: 120, on: .main, in: .common).autoconnect()
var body: some Scene {
WindowGroup {
@ -73,19 +75,30 @@ struct VernissageApp: App {
let accountModel = AccountModel(accountData: accountData)
self.applicationState.account = accountModel
self.applicationState.lastSeenStatusId = accountData.lastSeenStatusId
self.applicationState.lastBackgroundRefresh = nil
self.applicationState.amountOfNewStatuses = 0
self.client.setAccount(account: accountModel)
self.applicationViewMode = .mainView
// Check amount of newly added photos.
await self.loadInBackground()
}
}
}
.navigationViewStyle(.stack)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
try? HapticService.shared.start()
Task {
await self.loadInBackground()
}
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
HapticService.shared.stop()
}
.onReceive(timer) { time in
Task {
await self.loadInBackground()
}
}
.onChange(of: applicationState.theme) { newValue in
self.theme = newValue.colorScheme()
}
@ -145,6 +158,12 @@ struct VernissageApp: App {
defaultSettings.lastRefreshTokens = Date.now
CoreDataHandler.shared.save()
}
private func loadInBackground() async {
if let account = self.applicationState.account {
self.applicationState.amountOfNewStatuses = await HomeTimelineService.shared.amountOfNewStatuses(for: account)
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {

View File

@ -6,15 +6,23 @@
import SwiftUI
private struct OffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
}
struct HomeFeedView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var routerPath: RouterPath
@State private var amountOfNewStatuses = 0
@State private var allItemsLoaded = false
@State private var state: ViewState = .loading
@State private var opacity = 0.0
@State private var offset = -50.0
@FetchRequest var dbStatuses: FetchedResults<StatusData>
init(accountId: String) {
@ -49,6 +57,10 @@ struct HomeFeedView: View {
private func timeline() -> some View {
ZStack {
ScrollView {
// Offset reader for hiding top pill with amount of new photos.
self.offsetReader()
// VStack with all photos from database.
LazyVStack {
ForEach(dbStatuses, id: \.self) { item in
if self.shouldUpToDateBeVisible(statusId: item.id) {
@ -75,30 +87,25 @@ struct HomeFeedView: View {
}
}
}
if self.amountOfNewStatuses > 0 {
self.newPahotosView()
.coordinateSpace(name: "frameLayer")
.onPreferenceChange(OffsetPreferenceKey.self) {(offset) in
self.calculateOpacity(offset: offset)
}
}
.task {
await self.loadInBackground()
self.newPahotosView()
.offset(y: self.offset)
.opacity(self.opacity)
}
.refreshable {
await self.refreshData()
}
}
private func loadInBackground() async {
// Refreshing in background each 1 minute.
if let lastBackgroundRefresh = self.applicationState.lastBackgroundRefresh {
guard let refreshDate = Calendar.current.date(byAdding: .minute, value: 1, to: lastBackgroundRefresh), refreshDate < Date.now else {
return
self.applicationState.amountOfNewStatuses = 0
Task {
await self.refreshData()
}
}
if let account = self.applicationState.account {
self.amountOfNewStatuses = await HomeTimelineService.shared.amountOfNewStatuses(for: account)
self.applicationState.lastBackgroundRefresh = Date.now
.onChange(of: self.applicationState.amountOfNewStatuses) { newValue in
self.calculateOffset()
}.onAppear {
self.calculateOffset()
}
}
@ -107,10 +114,8 @@ struct HomeFeedView: View {
if let account = self.applicationState.account {
if let lastSeenStatusId = try await HomeTimelineService.shared.loadOnTop(for: account) {
try await HomeTimelineService.shared.save(lastSeenStatusId: lastSeenStatusId, for: account)
self.applicationState.lastSeenStatusId = lastSeenStatusId
self.amountOfNewStatuses = 0
self.applicationState.lastBackgroundRefresh = Date.now
}
}
} catch {
@ -124,9 +129,7 @@ struct HomeFeedView: View {
_ = try await HomeTimelineService.shared.loadOnTop(for: account)
}
self.amountOfNewStatuses = 0
self.applicationState.lastBackgroundRefresh = Date.now
self.applicationState.amountOfNewStatuses = 0
self.state = .loaded
} catch {
if !Task.isCancelled {
@ -138,10 +141,64 @@ struct HomeFeedView: View {
}
}
private func calculateOpacity(offset: CGFloat) {
if self.applicationState.amountOfNewStatuses == 0 {
return
}
// View is scrolled down.
if offset <= 0 {
self.opacity = 1.0
return
}
// View is scrolled up (loader is visible).
self.opacity = 1.0 - min((offset / 50.0), 1.0)
// View is scrolled so high that we can hide view.
if offset > 170 {
self.hideNewStatusesView()
}
}
private func calculateOffset() {
if self.applicationState.amountOfNewStatuses > 0 {
withAnimation(.easeIn) {
self.showNewStatusesView()
}
} else {
withAnimation(.easeOut) {
self.hideNewStatusesView()
}
}
}
private func showNewStatusesView() {
self.offset = 0.0
self.opacity = 1.0
}
private func hideNewStatusesView() {
self.offset = -50.0
self.opacity = 0.0
}
private func shouldUpToDateBeVisible(statusId: String) -> Bool {
return self.applicationState.lastSeenStatusId != dbStatuses.first?.id && self.applicationState.lastSeenStatusId == statusId
}
@ViewBuilder
private func offsetReader() -> some View {
GeometryReader { proxy in
Color.clear
.preference(
key: OffsetPreferenceKey.self,
value: proxy.frame(in: .named("frameLayer")).minY
)
}
.frame(height: 0)
}
@ViewBuilder
private func upToDatePlaceholder() -> some View {
VStack(alignment: .center) {
@ -163,17 +220,15 @@ struct HomeFeedView: View {
private func newPahotosView() -> some View {
VStack(alignment: .trailing, spacing: 4) {
HStack {
Spacer()
HStack {
Text("\(amountOfNewStatuses) new \(amountOfNewStatuses == 1 ? "photo" : "photos")")
}
.padding(4)
.font(.footnote)
.foregroundColor(Color.white.opacity(0.7))
.background(.ultraThinMaterial)
.clipShape(Capsule())
Image(systemName: "arrow.up")
Text("\(self.applicationState.amountOfNewStatuses) New \(self.applicationState.amountOfNewStatuses == 1 ? "Photo" : "Photos")")
}
.padding(12)
.font(.footnote)
.fontWeight(.light)
.foregroundColor(Color.white)
.background(.ultraThinMaterial)
.clipShape(Capsule())
Spacer()
}

View File

@ -157,7 +157,7 @@ struct MainView: View {
self.applicationState.account = accountModel
self.client.setAccount(account: accountModel)
self.applicationState.lastSeenStatusId = account.lastSeenStatusId
self.applicationState.lastBackgroundRefresh = nil
self.applicationState.amountOfNewStatuses = 0
ApplicationSettingsHandler.shared.setAccountAsDefault(accountData: account)
} label: {