diff --git a/app/Shared/Models/NotificationModel.swift b/app/Shared/Models/NotificationModel.swift index a5be17dc..0d6fd3fa 100644 --- a/app/Shared/Models/NotificationModel.swift +++ b/app/Shared/Models/NotificationModel.swift @@ -73,7 +73,7 @@ class NotificationModel: ObservableObject { if self?.showing == true { self?.objectWillChange.send() } } .store(in: &cancellables) - dataSource.$refreshedTimes + dataSource.$lastRefreshTime .map { _ in self.dataSource.items.filter { n in n.read == false }.count } .prepend(0) .pairwise() diff --git a/app/Shared/Models/PagingDataSource.swift b/app/Shared/Models/PagingDataSource.swift index 2e59ec47..bf9e03bf 100644 --- a/app/Shared/Models/PagingDataSource.swift +++ b/app/Shared/Models/PagingDataSource.swift @@ -26,7 +26,7 @@ class PagingDataSource: ObservableObject { @Published var isLoading = false @Published var isRefreshing = false @Published var latestResponse: Res? - @Published var refreshedTimes = 0 + @Published var lastRefreshTime: Date? @Published var loadFromPage: Int? private var loadedPage = 0 @@ -39,7 +39,7 @@ class PagingDataSource: ObservableObject { var nextPage: Int? { hasMore ? loadedPage + 1 : nil } var isInitialLoading: Bool { isLoading && loadedPage == 0 } var firstLoadedPage: Int? { itemToIndexAndPage.values.map { $0.page }.min() } - var notLoaded: Bool { items.isEmpty && refreshedTimes == 0 } + var notLoaded: Bool { items.isEmpty && lastRefreshTime == nil } init( buildRequest: @escaping (_ page: Int) -> AsyncRequest.OneOf_Value, @@ -150,7 +150,7 @@ class PagingDataSource: ObservableObject { self.totalPages = newTotalPages ?? self.totalPages self.loadedPage += 1 - self.refreshedTimes += 1 + self.lastRefreshTime = Date() } private func onRefreshError(e: LogicError, animated: Bool) { @@ -261,7 +261,7 @@ class PagingDataSource: ObservableObject { extension View { - func refreshable(dataSource: PagingDataSource, iOS15Only: Bool = false) -> some View { + func refreshable(dataSource: PagingDataSource, iOS15Only: Bool = false, refreshWhenEnterForeground: Bool = false) -> some View { #if canImport(SwiftUIRefresh) Group { if #available(iOS 15.0, *) { @@ -275,6 +275,14 @@ extension View { } else { self } + } .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in + guard refreshWhenEnterForeground else { return } + guard let last = dataSource.lastRefreshTime else { return } + + let elasped = Date().timeIntervalSince(last) + if elasped > 60 * 60 { // 1 hour elapsed + dataSource.refresh() + } } #else self diff --git a/app/Shared/Views/PostRowView.swift b/app/Shared/Views/PostRowView.swift index 3d97e129..21ab7e29 100644 --- a/app/Shared/Views/PostRowView.swift +++ b/app/Shared/Views/PostRowView.swift @@ -168,7 +168,7 @@ struct PostRowView: View { } var body: some View { - VStack(alignment: .leading, spacing: 10) { + let body = VStack(alignment: .leading, spacing: 10) { header content footer @@ -184,8 +184,21 @@ struct PostRowView: View { #endif .onAppear { self.post.attachments.map(\.url).forEach(attachments.add(_:)) } .environmentObject(attachments) + + if #available(iOS 15.0, *), let model = postReply { + body + .swipeActions(edge: .leading) { Button(action: { self.doQuote(model: model) }) { Image(systemName: "quote.bubble") } .tint(.accentColor) } + } else { + body + } } + func onLongPress() { + if let model = postReply { + HapticUtils.play(style: .medium) + doQuote(model: model) + } + } func doVote(_ operation: PostVoteRequest.Operation) { logicCallAsync(.postVote(.with { diff --git a/app/Shared/Views/TopicDetailsView.swift b/app/Shared/Views/TopicDetailsView.swift index abd89010..aee6fa27 100644 --- a/app/Shared/Views/TopicDetailsView.swift +++ b/app/Shared/Views/TopicDetailsView.swift @@ -159,12 +159,6 @@ struct TopicDetailsView: View { @ViewBuilder var moreMenu: some View { Menu { - #if os(iOS) - Section { - replyButton - } - #endif - Section { if enableAuthorOnly { Button(action: { self.action.navigateToAuthorOnly = self.topic.authorID }) { @@ -226,7 +220,10 @@ struct TopicDetailsView: View { Label("See Full Topic", systemImage: "doc.richtext") } } else { - moreMenu + HStack { + replyButton + moreMenu + } } } @@ -393,8 +390,8 @@ struct TopicDetailsView: View { @ToolbarContentBuilder var toolbar: some ToolbarContent { #if os(iOS) + ToolbarItem(placement: .mayNavigationBarLeading) { loadFirstPageButton } ToolbarItem(placement: .navigationBarTrailing) { progress } - ToolbarItem(placement: .navigationBarTrailing) { loadFirstPageButton } ToolbarItem(placement: .navigationBarTrailing) { menu } #elseif os(macOS) ToolbarItemGroup { @@ -427,7 +424,7 @@ struct TopicDetailsView: View { } } .mayGroupedListStyle() .withTopicDetailsAction(action: action) - .onReceive(dataSource.$refreshedTimes) { _ in mayScrollToJumpFloor() } + .onReceive(dataSource.$lastRefreshTime) { _ in mayScrollToJumpFloor() } .sheet(isPresented: $showJumpSelector) { TopicJumpSelectorView(maxFloor: maxFloor, initialFloor: floorToJump ?? 0, floorToJump: $floorToJump, pageToJump: $dataSource.loadFromPage) } } diff --git a/app/Shared/Views/TopicListView.swift b/app/Shared/Views/TopicListView.swift index 7c77ab71..0a5d70bf 100644 --- a/app/Shared/Views/TopicListView.swift +++ b/app/Shared/Views/TopicListView.swift @@ -249,7 +249,7 @@ struct TopicListView: View { } } } - .refreshable(dataSource: dataSource) + .refreshable(dataSource: dataSource, refreshWhenEnterForeground: true) .mayGroupedListStyle() } diff --git a/app/Shared/Views/UserView.swift b/app/Shared/Views/UserView.swift index 209581d5..3507d6bb 100644 --- a/app/Shared/Views/UserView.swift +++ b/app/Shared/Views/UserView.swift @@ -65,12 +65,12 @@ struct UserView: View { @ViewBuilder var avatar: some View { - if let user = self.user, let action = self.action { - Button(action: { action.showUserProfile = user }) { + if style == .huge, let url = avatarURL { + Button(action: { viewingImage.show(url: url) }) { avatarInner } .buttonStyle(PlainButtonStyle()) - } else if style == .huge, let url = avatarURL { - Button(action: { viewingImage.show(url: url) }) { + } else if let user = self.user, let action = self.action { + Button(action: { action.showUserProfile = user }) { avatarInner } .buttonStyle(PlainButtonStyle()) } else { @@ -117,17 +117,20 @@ struct UserView: View { Text("\(user?.postNum ?? 0)") .redacted(if: badUser) } .foregroundColor((1..<50 ~= user?.postNum ?? 50) ? .red : .secondary) - HStack(spacing: 2) { - Image(systemName: "calendar") - Text(Date(timeIntervalSince1970: TimeInterval(user?.regDate ?? 0)), style: .date) - .redacted(if: badUser) - } + HStack(spacing: 2) { Image(systemName: "flag") - Text("\((user?.fame ?? 0) / 10)") + Text(String(format: "%.01f", Double(user?.fame ?? 0) / 10.0)) .redacted(if: badUser) } .foregroundColor((user?.fame ?? 0 < 0) ? .red : .secondary) + if style == .huge { + HStack(spacing: 2) { + Image(systemName: "calendar") + Text(Date(timeIntervalSince1970: TimeInterval(user?.regDate ?? 0)), style: .date) + .redacted(if: badUser) + } + } } .font(.footnote) .foregroundColor(.secondary) }