Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tab navigation custom behaviours #932

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions Mlem.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions Mlem/API/APIClient/APIClient+Post.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ extension APIClient {
return SuccessResponse(from: compatibilityResponse)
}

func markPostsAsRead(for postIds: [Int], read: Bool) async throws -> SuccessResponse {
let request = try MarkPostReadRequest(session: session, postIds: postIds, read: read)
// TODO: 0.18 deprecation simply return result of perform
let compatibilityResponse = try await perform(request: request)
return SuccessResponse(from: compatibilityResponse)
}

func loadPost(id: Int, commentId: Int? = nil) async throws -> APIPostView {
let request = try GetPostRequest(session: session, id: id, commentId: commentId)
return try await perform(request: request).postView
Expand Down
12 changes: 12 additions & 0 deletions Mlem/API/APIClient/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,18 @@ extension APIClient {
return try await perform(request: request)
}

func banPerson(id: Int, shouldBan: Bool, expires: Int?, reason: String?, removeData: Bool) async throws -> BanPersonResponse {
let request = try BanPersonRequest(
session: session,
personId: id,
ban: shouldBan,
expires: expires,
reason: reason,
removeData: removeData
)
return try await perform(request: request)
}

func markPersonMentionAsRead(mentionId: Int, isRead: Bool) async throws -> APIPersonMentionView {
let request = try MarkPersonMentionAsRead(session: session, personMentionId: mentionId, read: isRead)
return try await perform(request: request).personMentionView
Expand Down
4 changes: 2 additions & 2 deletions Mlem/API/Models/Common/SuccessResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ struct SuccessResponse: Decodable {
}

struct MarkReadCompatibilityResponse: Decodable {
let success: Bool?
let postView: APIPostView?
let success: Bool? // 0.19+ response
let postView: APIPostView? // 0.18- response
}

struct SaveUserSettingsCompatibilityResponse: Decodable {
Expand Down
50 changes: 50 additions & 0 deletions Mlem/API/Requests/Person/BanPerson.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// BanPerson.swift
// Mlem
//
// Created by Sjmarf on 27/01/2024.
//

import Foundation

struct BanPersonRequest: APIPostRequest {
typealias Response = BanPersonResponse

let instanceURL: URL
let path = "user/ban"
let body: Body

struct Body: Encodable {
let personId: Int
let ban: Bool
let expires: Int?
let reason: String?
let removeData: Bool
let auth: String
}

init(
session: APISession,
personId: Int,
ban: Bool,
expires: Int?,
reason: String?,
removeData: Bool
) throws {
self.instanceURL = try session.instanceUrl

self.body = .init(
personId: personId,
ban: ban,
expires: expires,
reason: reason,
removeData: removeData,
auth: try session.token
)
}
}

struct BanPersonResponse: Decodable {
let banned: Bool
let personView: APIPersonView
}
22 changes: 20 additions & 2 deletions Mlem/API/Requests/Post/MarkPostRead.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ struct MarkPostReadRequest: APIPostRequest {
let body: Body

struct Body: Encodable {
let post_id: Int
let post_id: Int?
let post_ids: [Int]?
let read: Bool
let auth: String
// TODO: 0.19 support add post_ids? field
}

/// Create a request to mark a single post as read
init(
session: APISession,
postId: Int,
Expand All @@ -30,6 +31,23 @@ struct MarkPostReadRequest: APIPostRequest {

self.body = try .init(
post_id: postId,
post_ids: nil,
read: read,
auth: session.token
)
}

/// Create a request to mark multiple posts as read
init(
session: APISession,
postIds: [Int],
read: Bool
) throws {
self.instanceURL = try session.instanceUrl

self.body = try .init(
post_id: nil,
post_ids: postIds,
read: read,
auth: session.token
)
Expand Down
24 changes: 20 additions & 4 deletions Mlem/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ struct ContentView: View {
@Dependency(\.hapticManager) var hapticManager
@Dependency(\.siteInformation) var siteInformation
@Dependency(\.accountsTracker) var accountsTracker
@Dependency(\.markReadBatcher) var markReadBatcher

@Environment(\.setAppFlow) private var setFlow

Expand Down Expand Up @@ -152,6 +153,14 @@ struct ContentView: View {
.presentationDragIndicator(.hidden)
._presentationBackgroundInteraction(enabledUpThrough: .medium)
}
.sheet(item: $editorTracker.banUser) { editing in
NavigationStack {
BanUserView(editModel: editing)
}
.presentationDetents([.medium, .large], selection: .constant(.large))
.presentationDragIndicator(.hidden)
._presentationBackgroundInteraction(enabledUpThrough: .medium)
}
.sheet(item: $quickLookState.url) { url in
NavigationStack {
ImageDetailView(url: url)
Expand All @@ -162,12 +171,19 @@ struct ContentView: View {
.environmentObject(unreadTracker)
.environmentObject(quickLookState)
.onChange(of: scenePhase) { phase in
// when app moves into background, hide the account switcher. This prevents the app from reopening with the switcher enabled.
if phase != .active {
// prevents the app from reopening with the switcher enabled.
isPresentingAccountSwitcher = false
}
if phase == .background || phase == .inactive, appLock != .disabled {
biometricUnlock.isUnlocked = false

// flush batcher(s) to avoid batches being lost on quit
Task {
await markReadBatcher.flush()
}

// activate biometric lock
if appLock != .disabled {
biometricUnlock.isUnlocked = false
}
}
}
.fullScreenCover(isPresented: .constant(isAppLocked)) {
Expand Down
20 changes: 20 additions & 0 deletions Mlem/Dependency/MarkReadBatcher+Dependency.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// MarkReadBatcher+Dependency.swift
// Mlem
//
// Created by Eric Andrews on 2024-02-09.
//

import Dependencies
import Foundation

extension MarkReadBatcher: DependencyKey {
static let liveValue = MarkReadBatcher()
}

extension DependencyValues {
var markReadBatcher: MarkReadBatcher {
get { self[MarkReadBatcher.self] }
set { self[MarkReadBatcher.self] = newValue }
}
}
8 changes: 8 additions & 0 deletions Mlem/Enums/Settings/PostSize.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ extension PostSize: SettingsOptions {
}

var id: Self { self }

var markReadThreshold: Int {
switch self {
case .compact: 4
case .headline: 2
case .large: 1
}
}
}

extension PostSize: AssociatedIcon {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ struct DestructiveConfirmation: ViewModifier {
}
}
} message: {
if let destructivePrompt = confirmationMenuFunction?.destructiveActionPrompt {
Text(destructivePrompt)
if case let .destructive(prompt: prompt) = confirmationMenuFunction?.role, let prompt {
Text(prompt)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Mlem/Extensions/View Modifiers/View+HandleLemmyLinks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ struct HandleLemmyLinksDisplay: ViewModifier {
UserView(user: user, communityContext: communityContext)
.environmentObject(appState)
.environmentObject(quickLookState)
case let .instance(domainName, instance):
InstanceView(domainName: domainName, instance: instance)
case let .instance(instance):
InstanceView(instance: instance)
case let .postLinkWithContext(postLink):
ExpandedPost(post: postLink.post, community: postLink.community, scrollTarget: postLink.scrollTarget)
.environmentObject(postLink.postTracker)
Expand Down
9 changes: 8 additions & 1 deletion Mlem/Icons.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ enum Icons {
static let titleOnlyPost: String = "character.bubble"
static let pinned: String = "pin.fill"
static let websiteIcon: String = "globe"
static let hideRead: String = "book"
static let read: String = "book"

// post sizes
static let postSizeSetting: String = "rectangle.expand.vertical"
Expand Down Expand Up @@ -149,6 +149,11 @@ enum Icons {
static let close: String = "multiply"
static let cakeDay: String = "birthday.cake"

// uptime
static let uptimeOffline: String = "xmark.circle.fill"
static let uptimeOnline: String = "checkmark.circle.fill"
static let uptimeOutage: String = "exclamationmark.circle.fill"

// end of feed
static let endOfFeedHobbit: String = "figure.climbing"
static let endOfFeedCartoon: String = "figure.wave"
Expand Down Expand Up @@ -190,6 +195,8 @@ enum Icons {
static let limitImageHeightSetting: String = "rectangle.compress.vertical"
static let appLockSettings: String = "lock.app.dashed"
static let collapseComments: String = "arrow.down.and.line.horizontal.and.arrow.up"
static let ban: String = "xmark.circle"
static let tabBarNavigation: String = "chevron.backward.circle"

// misc
static let `private`: String = "lock"
Expand Down
2 changes: 1 addition & 1 deletion Mlem/Logic/BiometricUnlock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class BiometricUnlock: ObservableObject {
let reason = "Please authenticate to unlock app."

context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, _ in
DispatchQueue.main.sync {
DispatchQueue.main.async {
if success {
self.isUnlocked = true
onComplete(.success(()))
Expand Down
75 changes: 75 additions & 0 deletions Mlem/Models/Batchers/MarkReadBatcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// MarkReadBatcher.swift
// Mlem
//
// Created by Eric Andrews on 2024-02-08.
//

import Dependencies
import Foundation
import Semaphore

class MarkReadBatcher {
@Dependency(\.notifier) var notifier
@Dependency(\.errorHandler) var errorHandler
@Dependency(\.postRepository) var postRepository

private let loadingSemaphore: AsyncSemaphore = .init(value: 1)

private(set) var enabled: Bool = false
private var pending: [Int] = .init()
private var sending: [Int] = .init()

func resolveSiteVersion(to siteVersion: SiteVersion) {
enabled = siteVersion >= .init("0.19.0")
}

func flush() async {
// only one thread may execute this function at a time to avoid duplicate requests
await loadingSemaphore.wait()
defer { loadingSemaphore.signal() }

sending = pending
pending = .init()

// perform this on background thread to return ASAP
Task {
await dispatchSending()
}
}

func dispatchSending() async {
guard sending.count > 0 else {
return
}

do {
try await postRepository.markRead(postIds: sending, read: true)
} catch {
errorHandler.handle(error)
}

sending = .init()
}

func add(_ postId: Int) async {
// FUTURE DEV: wouldn't it be nicer to pass in a PostModel and perform the mark read state fake here?
// PAST DEV: no, that causes nasty little memory errors in fringe cases thanks to pass-by-reference. Trust in the safety of pass-by-value, future dev.

guard enabled else {
assertionFailure("Cannot add to disabled batcher!")
return
}

if pending.count >= 50 {
await flush()
}

// This call is deliberately placed *after* the flush check to avoid the potential data race:
// - Threads 0 and 1 call add() at the same time
// - Thread 0 calls flush() and performs sending = pending
// - Thread 1 adds its id to pending
// - Thread 0 performs pending = .init(), and thread 1's id is lost forever!
pending.append(postId)
}
}
18 changes: 18 additions & 0 deletions Mlem/Models/Composers/Administration/BanUserEditorModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// BanUser.swift
// Mlem
//
// Created by Sjmarf on 26/01/2024.
//

import Dependencies
import Foundation
import SwiftUI

struct BanUserEditorModel: Identifiable {
@Dependency(\.commentRepository) var commentRepository

let user: UserModel
var callback: (_ item: UserModel) -> Void = { _ in }
var id: Int { user.userId }
}
Loading
Loading