Skip to content

Commit

Permalink
Search context menus (#705)
Browse files Browse the repository at this point in the history
Co-authored-by: Bosco Ho <[email protected]>
(cherry picked from commit 3a2a7c0)
  • Loading branch information
Sjmarf authored and boscojwho committed Nov 23, 2023
1 parent 3e5263e commit 8ca776a
Show file tree
Hide file tree
Showing 16 changed files with 301 additions and 53 deletions.
12 changes: 12 additions & 0 deletions Mlem.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
03A1B3F42A83F46200AB0DE0 /* ShareButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A1B3F32A83F46200AB0DE0 /* ShareButtonView.swift */; };
03A1B3F72A84000400AB0DE0 /* APIContentAggregatesProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A1B3F62A84000400AB0DE0 /* APIContentAggregatesProtocol.swift */; };
03A1B3F92A8400DD00AB0DE0 /* APIContentViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A1B3F82A8400DD00AB0DE0 /* APIContentViewProtocol.swift */; };
03A276792AFD903600C0D66B /* CommunityModel+MenuFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A276782AFD903600C0D66B /* CommunityModel+MenuFunctions.swift */; };
03A2767B2AFE560000C0D66B /* CommunityModel+SwipeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A2767A2AFE560000C0D66B /* CommunityModel+SwipeActions.swift */; };
03A2767D2AFE656700C0D66B /* UserModel+MenuFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A2767C2AFE656700C0D66B /* UserModel+MenuFunctions.swift */; };
03A40DAD2AD5EA11005F019F /* NoPostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A40DAC2AD5EA11005F019F /* NoPostsView.swift */; };
03B643572A6864CD00F65700 /* TabBarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B643562A6864CD00F65700 /* TabBarSettingsView.swift */; };
03B7AAEF2ABCB9DC00068B23 /* ContentTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B7AAEE2ABCB9DC00068B23 /* ContentTracker.swift */; };
Expand Down Expand Up @@ -554,6 +557,9 @@
03A1B3F32A83F46200AB0DE0 /* ShareButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareButtonView.swift; sourceTree = "<group>"; };
03A1B3F62A84000400AB0DE0 /* APIContentAggregatesProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIContentAggregatesProtocol.swift; sourceTree = "<group>"; };
03A1B3F82A8400DD00AB0DE0 /* APIContentViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIContentViewProtocol.swift; sourceTree = "<group>"; };
03A276782AFD903600C0D66B /* CommunityModel+MenuFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommunityModel+MenuFunctions.swift"; sourceTree = "<group>"; };
03A2767A2AFE560000C0D66B /* CommunityModel+SwipeActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommunityModel+SwipeActions.swift"; sourceTree = "<group>"; };
03A2767C2AFE656700C0D66B /* UserModel+MenuFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserModel+MenuFunctions.swift"; sourceTree = "<group>"; };
03A40DAC2AD5EA11005F019F /* NoPostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoPostsView.swift; sourceTree = "<group>"; };
03B643562A6864CD00F65700 /* TabBarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarSettingsView.swift; sourceTree = "<group>"; };
03B7AAEE2ABCB9DC00068B23 /* ContentTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTracker.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1155,6 +1161,7 @@
children = (
03B7AAF22ABEF85200068B23 /* UserModel.swift */,
030D00872AD1BB2600953B1D /* UserModel+ContentModel.swift */,
03A2767C2AFE656700C0D66B /* UserModel+MenuFunctions.swift */,
);
path = User;
sourceTree = "<group>";
Expand Down Expand Up @@ -1254,6 +1261,8 @@
isa = PBXGroup;
children = (
03EEEAF82ABB985D0087F8D8 /* CommunityModel.swift */,
03A276782AFD903600C0D66B /* CommunityModel+MenuFunctions.swift */,
03A2767A2AFE560000C0D66B /* CommunityModel+SwipeActions.swift */,
03FD64FE2AE53D0E00957AA9 /* CommunityModel+ContentModel.swift */,
);
path = Community;
Expand Down Expand Up @@ -2874,6 +2883,7 @@
637218582A3A2AAD008C4816 /* APISite.swift in Sources */,
B11D72832A49FAA7009DC22F /* Cached Image.swift in Sources */,
637218752A3A2AAD008C4816 /* GetCommunity.swift in Sources */,
03A2767B2AFE560000C0D66B /* CommunityModel+SwipeActions.swift in Sources */,
CDF1EF142A6B6D6E003594B6 /* Feed View Logic.swift in Sources */,
6DFF50432A48DED3001E648D /* Inbox View.swift in Sources */,
03EEEAF72AB8ED3C0087F8D8 /* SearchTabPicker.swift in Sources */,
Expand Down Expand Up @@ -3037,6 +3047,7 @@
CDDCF6452A66375E003DA3AC /* FancyTabItemViewModifier.swift in Sources */,
6DA61F872A5720EA001EA633 /* RecentSearchesTracker.swift in Sources */,
CD4368DD2AE24E1A00BD8BD1 /* InboxView+Logic.swift in Sources */,
03A276792AFD903600C0D66B /* CommunityModel+MenuFunctions.swift in Sources */,
637218762A3A2AAD008C4816 /* BlockCommunity.swift in Sources */,
03CB329E2A6D8E910021EF27 /* PostDetailEditorView.swift in Sources */,
CD69F5752A42479A0028D4F7 /* Comment Item Logic.swift in Sources */,
Expand Down Expand Up @@ -3126,6 +3137,7 @@
6363D5FA27EE1BDA00E34822 /* Settings View.swift in Sources */,
CDDCF6532A677F45003DA3AC /* TabSelection.swift in Sources */,
CD4368C42AE240B100BD8BD1 /* MentionModel.swift in Sources */,
03A2767D2AFE656700C0D66B /* UserModel+MenuFunctions.swift in Sources */,
6372184D2A3A2AAD008C4816 /* APIErrorResponse.swift in Sources */,
CD7B53B92A5F263D00006E81 /* APIPrivateMessageReport.swift in Sources */,
637218602A3A2AAD008C4816 /* EditPost.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions Mlem/App Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ struct AppConstants {
// MARK: - Text

static let blockUserPrompt: String = "Really block this user?"
static let blockCommunityPrompt: String = "Really block this community?"
static let reportPostPrompt: String = "Really report this post?"
static let reportCommentPrompt: String = "Really report this comment?"
static let reportMessagePrompt: String = "Really report this message?"
Expand Down
11 changes: 2 additions & 9 deletions Mlem/Extensions/URL - Lemmy Image Parameters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,10 @@
import Foundation

extension URL {
// Returns a "small" version of the icon
// Spec described here: https://join-lemmy.org/docs/contributors/04-api.html#images
var withIcon32Parameters: URL {
func withIconSize(_ size: Int) -> URL {
var result = self
result.append(queryItems: [URLQueryItem(name: "thumbnail", value: "32")])
return result
}

var withIcon64Parameters: URL {
var result = self
result.append(queryItems: [URLQueryItem(name: "thumbnail", value: "64")])
result.append(queryItems: [URLQueryItem(name: "thumbnail", value: "\(size)")])
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ extension CommunityModel: ContentModel {
var uid: ContentModelIdentifier { .init(contentType: .community, contentId: communityId) }
var imageUrls: [URL] {
if let url = avatar {
return [url.withIcon64Parameters]
return [url.withIconSize(128)]
}
return []
}
Expand Down
68 changes: 68 additions & 0 deletions Mlem/Models/Content/Community/CommunityModel+MenuFunctions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// CommunityModel+MenuFunctions.swift
// Mlem
//
// Created by Sjmarf on 09/11/2023.
//

import Foundation
import SwiftUI

extension CommunityModel {
func subscribeMenuFunction(_ callback: @escaping (_ item: Self) -> Void = { _ in }) throws -> MenuFunction {
guard let subscribed else {
throw CommunityError.noData
}
return .standardMenuFunction(
text: subscribed ? "Unsubscribe" : "Subscribe",
imageName: subscribed ? Icons.unsubscribe : Icons.subscribe,
destructiveActionPrompt: subscribed ? "Are you sure you want to unsubscribe from \(name)?" : nil,
enabled: true,
callback: {
Task {
var new = self
do {
try await new.toggleSubscribe(callback)
} catch {
errorHandler.handle(error)
}
}
}
)
}

func blockMenuFunction(_ callback: @escaping (_ item: Self) -> Void = { _ in }) throws -> MenuFunction {
guard let blocked else {
throw CommunityError.noData
}
return .standardMenuFunction(
text: blocked ? "Unblock" : "Block",
imageName: blocked ? Icons.show : Icons.hide,
destructiveActionPrompt: blocked ? nil : AppConstants.blockCommunityPrompt,
enabled: true,
callback: {
Task {
var new = self
do {
try await new.toggleBlock(callback)
} catch {
errorHandler.handle(error)
}
}
}
)
}

func menuFunctions(_ callback: @escaping (_ item: Self) -> Void = { _ in }) -> [MenuFunction] {
var functions: [MenuFunction] = .init()
if let function = try? subscribeMenuFunction(callback) {
functions.append(function)
}
functions.append(.shareMenuFunction(url: communityUrl))
if let function = try? blockMenuFunction(callback) {
functions.append(function)
}

return functions
}
}
56 changes: 56 additions & 0 deletions Mlem/Models/Content/Community/CommunityModel+SwipeActions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// CommunityModel+SwipeActions.swift
// Mlem
//
// Created by Sjmarf on 10/11/2023.
//

import Foundation

extension CommunityModel {

func subscribeSwipeAction(
_ callback: @escaping (_ item: Self) -> Void = { _ in },
confirmDestructive: ((StandardMenuFunction) -> Void)? = nil
) throws -> SwipeAction {
guard let subscribed else {
throw CommunityError.noData
}
let (emptySymbolName, fullSymbolName) = (subscribed)
? (Icons.unsubscribePerson, Icons.unsubscribePersonFill)
: (Icons.subscribePerson, Icons.subscribePersonFill)
return SwipeAction(
symbol: .init(emptyName: emptySymbolName, fillName: fullSymbolName),
color: subscribed ? .red : .green,
action: {
Task {
hapticManager.play(haptic: .lightSuccess, priority: .low)

if subscribed, let confirmDestructive {
if case .standard(let function) = try? subscribeMenuFunction(callback) {
confirmDestructive(function)
}
} else {
var new = self
do {
try await new.toggleSubscribe(callback)
} catch {
errorHandler.handle(error)
}
}
}
}
)
}

func swipeActions(
_ callback: @escaping (_ item: Self) -> Void = { _ in },
confirmDestructive: ((StandardMenuFunction) -> Void)? = nil
) -> SwipeConfiguration {
var trailingActions: [SwipeAction] = []
if let action = try? subscribeSwipeAction(callback, confirmDestructive: confirmDestructive) {
trailingActions.append(action)
}
return SwipeConfiguration(leadingActions: [], trailingActions: trailingActions)
}
}
3 changes: 1 addition & 2 deletions Mlem/Models/Content/User/UserModel+ContentModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ import Foundation

extension UserModel: ContentModel {
var uid: ContentModelIdentifier { .init(contentType: .user, contentId: userId) }

var imageUrls: [URL] {
if let url = avatar {
return [url.withIcon64Parameters]
return [url.withIconSize(128)]
}
return []
}
Expand Down
32 changes: 32 additions & 0 deletions Mlem/Models/Content/User/UserModel+MenuFunctions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// UserModel+MenuFunctions.swift
// Mlem
//
// Created by Sjmarf on 10/11/2023.
//

import Foundation

extension UserModel {
func blockMenuFunction(_ callback: @escaping (_ item: Self) -> Void = { _ in }) -> MenuFunction {
return .standardMenuFunction(
text: blocked ? "Unblock" : "Block",
imageName: blocked ? Icons.show : Icons.hide,
destructiveActionPrompt: blocked ? nil : AppConstants.blockUserPrompt,
enabled: true,
callback: {
Task {
var new = self
await new.toggleBlock(callback)
}
}
)
}

func menuFunctions(_ callback: @escaping (_ item: Self) -> Void = { _ in }) -> [MenuFunction] {
var functions: [MenuFunction] = .init()
functions.append(.shareMenuFunction(url: profileUrl))
functions.append(blockMenuFunction(callback))
return functions
}
}
27 changes: 27 additions & 0 deletions Mlem/Models/Content/User/UserModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import Foundation
import SwiftUI

struct UserModel {
@Dependency(\.personRepository) var personRepository
@Dependency(\.hapticManager) var hapticManager
@Dependency(\.errorHandler) var errorHandler

@available(*, deprecated, message: "Use attributes of the UserModel directly instead.")
var person: APIPerson
Expand Down Expand Up @@ -48,6 +51,8 @@ struct UserModel {
var postCount: Int?
var commentCount: Int?

var blocked: Bool

static let developerNames = [
"https://lemmy.tespia.org/u/navi",
"https://beehaw.org/u/jojo",
Expand Down Expand Up @@ -94,6 +99,10 @@ struct UserModel {

self.profileUrl = person.actorId
self.sharedInboxUrl = person.sharedInboxLink

// Annoyingly, PersonView doesn't include whether the user is blocked so we can't
// actually determine this without making extra requests...
self.blocked = false
}

// Once we've done other model types we should stop this from relying on API types
Expand Down Expand Up @@ -127,6 +136,23 @@ struct UserModel {
}
return ret
}

mutating func toggleBlock(_ callback: @escaping (_ item: Self) -> Void = { _ in }) async {
blocked.toggle()
RunLoop.main.perform { [self] in
callback(self)
}
do {
let response = try await personRepository.updateBlocked(for: userId, blocked: blocked)
self.blocked = response.blocked
RunLoop.main.perform { [self] in
callback(self)
}
} catch {
hapticManager.play(haptic: .failure, priority: .high)
errorHandler.handle(error)
}
}
}

extension UserModel: Identifiable {
Expand All @@ -141,5 +167,6 @@ extension UserModel: Hashable {
/// Hashes all fields for which state changes should trigger view updates.
func hash(into hasher: inout Hasher) {
hasher.combine(uid)
hasher.combine(blocked)
}
}
4 changes: 2 additions & 2 deletions Mlem/Models/Trackers/Post Tracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -434,11 +434,11 @@ class PostTracker: ObservableObject {
// preload user and community avatars--fetching both because we don't know which we'll need, but these are super tiny
// so it's probably not an API crime, right?
if let communityAvatarLink = post.community.avatar {
imageRequests.append(ImageRequest(url: communityAvatarLink.withIcon64Parameters))
imageRequests.append(ImageRequest(url: communityAvatarLink.withIconSize(Int(AppConstants.smallAvatarSize*2))))
}

if let userAvatarLink = post.creator.avatar {
imageRequests.append(ImageRequest(url: userAvatarLink.withIcon64Parameters))
imageRequests.append(ImageRequest(url: userAvatarLink.withIconSize(Int(AppConstants.largeAvatarSize*2))))
}

switch post.postType {
Expand Down
5 changes: 5 additions & 0 deletions Mlem/Repositories/PersonRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,9 @@ class PersonRepository {
throw error
}
}

@discardableResult
func updateBlocked(for personId: Int, blocked: Bool) async throws -> BlockPersonResponse {
try await apiClient.blockPerson(id: personId, shouldBlock: blocked)
}
}
Loading

0 comments on commit 8ca776a

Please sign in to comment.