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

Automatic collapse child comments #841

Merged
merged 32 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5c36133
[Feature] Added automatic collapse child comments, including settings…
schmeat Jan 3, 2024
25a70ba
- Updated icon for collapse comment settings
schmeat Jan 3, 2024
71052be
Merge branch 'dev' into feature/collapse-child-comments
schmeat Jan 3, 2024
e74dd8a
- Added isCollapsed flag to parentCollapsed to make only parent comme…
schmeat Jan 3, 2024
155bc1d
- Updated logic for calculating when to hide comments automatically
schmeat Jan 3, 2024
e24c713
- Code clean up for auto collapse comments
schmeat Jan 3, 2024
b839613
- Updated text and location of Collapse comments in settings
schmeat Jan 3, 2024
67e60e9
- Switch back enum to struct
schmeat Jan 4, 2024
24980b0
- switch back enum to struct
schmeat Jan 4, 2024
0d513de
- switching enum to struct, third time's the charm?
schmeat Jan 4, 2024
ea36093
Merge branch 'dev' into feature/collapse-child-comments
schmeat Jan 4, 2024
c3a0ce0
Merge branch 'dev' into feature/collapse-child-comments
schmeat Jan 4, 2024
d5be159
- Rough logic and UI for collapse comments automatically with a bar b…
schmeat Jan 5, 2024
4212d58
- Updated appearance of the collapsed child comment text box
schmeat Jan 5, 2024
467929a
- Iterating through depth 1 comments to also uncollapse for comment r…
schmeat Jan 5, 2024
5aa5391
- added padding top and bottom to CollapsedCommentReplies
schmeat Jan 6, 2024
44ca122
- restricted height and changed colour to .accent for CollapsedCommen…
schmeat Jan 6, 2024
05e83b2
- refactored uncollapse comment logic into method
schmeat Jan 6, 2024
7f0b9e9
- Fixed logic for top level comment collapsing/uncollapsing
schmeat Jan 7, 2024
ccb8279
Update Mlem/Views/Shared/Comments/Components/CollapsedCommentReplies.…
schmeat Jan 7, 2024
722694f
- set isCommentReplyHidden to false when disappearing view so that it…
schmeat Jan 8, 2024
b11eeba
Update Mlem/Views/Shared/Comments/Comment Item.swift
schmeat Jan 8, 2024
32f55ef
- Check different child count to prevent child comment button from sh…
schmeat Jan 8, 2024
72d6cd9
- formatting
schmeat Jan 8, 2024
6317985
- Added page context to collapsing child comments, bypassing a lot of…
schmeat Jan 10, 2024
3830ba0
- formatting
schmeat Jan 10, 2024
347be9b
- re-added ondisappear for coming back from profile
schmeat Jan 11, 2024
f005efa
Merge branch 'dev' into feature/collapse-child-comments
schmeat Jan 14, 2024
67dd861
- added check before reloading comments on page (thanks sjmarf)
schmeat Jan 14, 2024
270f90c
Merge branch 'dev' into feature/collapse-child-comments
schmeat Jan 16, 2024
a4085d6
Merge branch 'dev' into feature/collapse-child-comments
schmeat Jan 17, 2024
ac1e036
Merge branch 'dev' of https://github.com/schmeat/mlem into feature/co…
schmeat Jan 29, 2024
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
4 changes: 4 additions & 0 deletions Mlem.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@
6DEB0FFB2A4F87BF007CAB99 /* User Moderator View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEB0FFA2A4F87BF007CAB99 /* User Moderator View.swift */; };
6DFF50432A48DED3001E648D /* Inbox View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DFF50422A48DED3001E648D /* Inbox View.swift */; };
6DFF50452A48E373001E648D /* GetPrivateMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DFF50442A48E373001E648D /* GetPrivateMessages.swift */; };
6FB4A4DE2B47860B00A7CD82 /* CollapsedCommentReplies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB4A4DD2B47860B00A7CD82 /* CollapsedCommentReplies.swift */; };
88B165B82A8643F4007C9115 /* View+NavigationBarColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B165B72A8643F4007C9115 /* View+NavigationBarColor.swift */; };
AD1B0D352A5F63F60006F554 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD1B0D342A5F63F60006F554 /* AboutView.swift */; };
AD1B0D372A5F7A260006F554 /* Licenses.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD1B0D362A5F7A260006F554 /* Licenses.swift */; };
Expand Down Expand Up @@ -847,6 +848,7 @@
6DEB0FFA2A4F87BF007CAB99 /* User Moderator View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "User Moderator View.swift"; sourceTree = "<group>"; };
6DFF50422A48DED3001E648D /* Inbox View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Inbox View.swift"; sourceTree = "<group>"; };
6DFF50442A48E373001E648D /* GetPrivateMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetPrivateMessages.swift; sourceTree = "<group>"; };
6FB4A4DD2B47860B00A7CD82 /* CollapsedCommentReplies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsedCommentReplies.swift; sourceTree = "<group>"; };
88B165B72A8643F4007C9115 /* View+NavigationBarColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+NavigationBarColor.swift"; sourceTree = "<group>"; };
AD1B0D342A5F63F60006F554 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
AD1B0D362A5F7A260006F554 /* Licenses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Licenses.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2075,6 +2077,7 @@
children = (
CDE6A80C2A45EAB30062D161 /* Embedded Post.swift */,
CD391F992A537EF900E213B5 /* CommentBodyView.swift */,
6FB4A4DD2B47860B00A7CD82 /* CollapsedCommentReplies.swift */,
);
path = Components;
sourceTree = "<group>";
Expand Down Expand Up @@ -3536,6 +3539,7 @@
6D15D74C2A44DC240061B5CB /* Date+RelativeTime.swift in Sources */,
CDA217E62A63016A00BDA173 /* ReportMessage.swift in Sources */,
CD9DD8832A622A6C0044EA8E /* ReportCommentReply.swift in Sources */,
6FB4A4DE2B47860B00A7CD82 /* CollapsedCommentReplies.swift in Sources */,
CD3FBCE12A4A836000B2063F /* AllItemsFeedView.swift in Sources */,
6D91D4582A4159D8006B8F9A /* FavoriteStarButtonStyle.swift in Sources */,
63F0C7B92A0533C700A18C5D /* Add Account View.swift in Sources */,
Expand Down
25 changes: 18 additions & 7 deletions Mlem/API/Internal/HierarchicalComment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,20 @@ class HierarchicalComment: ObservableObject {
/// Indicates whether the *current* comment is collapsed.
@Published var isCollapsed: Bool = false

init(comment: APICommentView, children: [HierarchicalComment], parentCollapsed: Bool, collapsed: Bool) {
init(
comment: APICommentView,
children: [HierarchicalComment],
parentCollapsed: Bool,
collapsed: Bool,
shouldCollapseChildren: Bool = false
) {
let depth = max(0, comment.comment.path.split(separator: ".").count - 2)

self.commentView = comment
self.children = children
self.depth = max(0, commentView.comment.path.split(separator: ".").count - 2)
self.isParentCollapsed = parentCollapsed
self.isCollapsed = collapsed
self.depth = depth
self.isParentCollapsed = shouldCollapseChildren && depth >= 1 || parentCollapsed
self.isCollapsed = shouldCollapseChildren && depth == 1 || collapsed
self.links = comment.comment.content.parseLinks()
}
}
Expand Down Expand Up @@ -161,23 +169,26 @@ extension [APICommentView] {
}

let identifiedComments = Dictionary(uniqueKeysWithValues: allComments.lazy.map { ($0.id, $0) })

let collapseChildComments = UserDefaults.standard.bool(forKey: "collapseChildComments")

/// Recursively populates child comments by looking up IDs from `childrenById`
func populateChildren(_ comment: APICommentView) -> HierarchicalComment {
guard let childIds = childrenById[comment.id] else {
return .init(
comment: comment,
children: [],
parentCollapsed: false,
collapsed: false
collapsed: false,
shouldCollapseChildren: collapseChildComments
)
}

let commentWithChildren = HierarchicalComment(
comment: comment,
children: [],
parentCollapsed: false,
collapsed: false
collapsed: false,
shouldCollapseChildren: collapseChildComments
)
commentWithChildren.children = childIds
.compactMap { id -> HierarchicalComment? in
Expand Down
2 changes: 1 addition & 1 deletion Mlem/API/Models/Site/APILocalSite.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ struct APILocalSite: Decodable {
// let legalInformation: String?
// let hideModlogModNames: Bool
// let applicationEmailAdmins: Bool
let slurFilterRegex: String?
let slurFilterRegex: String?
// let actorNameMaxLength: Int
// let federationEnabled: Bool
// let federationDebug: Bool
Expand Down
22 changes: 11 additions & 11 deletions Mlem/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,18 @@ struct ContentView: View {
}

ProfileView()
.fancyTabItem(tag: TabSelection.profile) {
FancyTabBarLabel(
tag: TabSelection.profile,
customText: appState.tabDisplayName,
symbolConfiguration: .init(
symbol: FancyTabBarLabel.SymbolConfiguration.profile.symbol,
activeSymbol: FancyTabBarLabel.SymbolConfiguration.profile.activeSymbol,
remoteSymbolUrl: appState.profileTabRemoteSymbolUrl
.fancyTabItem(tag: TabSelection.profile) {
FancyTabBarLabel(
tag: TabSelection.profile,
customText: appState.tabDisplayName,
symbolConfiguration: .init(
symbol: FancyTabBarLabel.SymbolConfiguration.profile.symbol,
activeSymbol: FancyTabBarLabel.SymbolConfiguration.profile.activeSymbol,
remoteSymbolUrl: appState.profileTabRemoteSymbolUrl
)
)
)
.simultaneousGesture(accountSwitchLongPress)
}
.simultaneousGesture(accountSwitchLongPress)
}

SearchRoot()
.fancyTabItem(tag: TabSelection.search) {
Expand Down
2 changes: 1 addition & 1 deletion Mlem/Icons.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation
import SwiftUI

/// SFSymbol names for icons
struct Icons {
enum Icons {
Sjmarf marked this conversation as resolved.
Show resolved Hide resolved
// votes
static let votes: String = "arrow.up.arrow.down.square"
static let upvote: String = "arrow.up"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ extension CommunityModel {
functions.append(.standard(function))
functions.append(.standard(favoriteMenuFunction(callback)))
}
if let instanceHost = self.communityUrl.host() {
if let instanceHost = communityUrl.host() {
let instance: InstanceModel?
if let site {
instance = .init(from: site)
Expand Down
14 changes: 7 additions & 7 deletions Mlem/Models/Content/Instance/InstanceModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,33 @@ struct InstanceModel {
var slurFilterRegex: Regex<AnyRegexOutput>?

init(from response: SiteResponse) {
self.update(with: response)
update(with: response)
}

init(from site: APISite) {
self.update(with: site)
update(with: site)
}

mutating func update(with response: SiteResponse) {
self.administrators = response.admins.map {
administrators = response.admins.map {
var user = UserModel(from: $0)
user.usesExternalData = true
user.isAdmin = true
return user
}
self.version = SiteVersion(response.version)
version = SiteVersion(response.version)

let localSite = response.siteView.localSite

do {
if let regex = localSite.slurFilterRegex {
self.slurFilterRegex = try .init(regex)
slurFilterRegex = try .init(regex)
}
} catch {
print("Invalid slur filter regex")
}

self.update(with: response.siteView.site)
update(with: response.siteView.site)
}

mutating func update(with site: APISite) {
Expand Down Expand Up @@ -82,7 +82,7 @@ extension InstanceModel: Identifiable {

extension InstanceModel: Hashable {
static func == (lhs: InstanceModel, rhs: InstanceModel) -> Bool {
return lhs.hashValue == rhs.hashValue
lhs.hashValue == rhs.hashValue
}

/// Hashes all fields for which state changes should trigger view updates.
Expand Down
1 change: 0 additions & 1 deletion Mlem/Models/Content/Post/PostModel+MenuFunctions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ extension PostModel {
await self.delete()
}
})

}

// Share
Expand Down
4 changes: 2 additions & 2 deletions Mlem/Models/Content/User/UserModel+MenuFunctions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation

extension UserModel {
func blockMenuFunction(_ callback: @escaping (_ item: Self) -> Void = { _ in }) -> MenuFunction {
return .standardMenuFunction(
.standardMenuFunction(
text: blocked ? "Unblock" : "Block",
imageName: blocked ? Icons.show : Icons.hide,
destructiveActionPrompt: blocked ? nil : AppConstants.blockUserPrompt,
Expand All @@ -25,7 +25,7 @@ extension UserModel {

func menuFunctions(_ callback: @escaping (_ item: Self) -> Void = { _ in }) -> [MenuFunction] {
var functions: [MenuFunction] = .init()
if let instanceHost = self.profileUrl.host() {
if let instanceHost = profileUrl.host() {
let instance: InstanceModel?
if let site {
instance = .init(from: site)
Expand Down
66 changes: 33 additions & 33 deletions Mlem/Models/Content/User/UserModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ struct UserModel {
/// Creates a UserModel from an GetPersonDetailsResponse
/// - Parameter response: GetPersonDetailsResponse to create a UserModel representation of
init(from response: GetPersonDetailsResponse) {
self.update(with: response)
update(with: response)
}

/// Creates a UserModel from an APIPersonView
/// - Parameter apiPersonView: APIPersonView to create a UserModel representation of
init(from personView: APIPersonView) {
self.update(with: personView)
update(with: personView)
}

/// Creates a UserModel from an APIPerson. Note that using this initialiser nullifies count values, since
Expand All @@ -90,55 +90,55 @@ struct UserModel {
}

mutating func update(with response: GetPersonDetailsResponse) {
self.moderatedCommunities = response.moderates.map { CommunityModel(from: $0.community) }
self.update(with: response.personView)
moderatedCommunities = response.moderates.map { CommunityModel(from: $0.community) }
update(with: response.personView)
}

mutating func update(with personView: APIPersonView) {
self.postCount = personView.counts.postCount
self.commentCount = personView.counts.commentCount
postCount = personView.counts.postCount
commentCount = personView.counts.commentCount

// TODO: 0.18 Deprecation
@Dependency(\.siteInformation) var siteInformation
if (siteInformation.version ?? .infinity) > .init("0.19.0") {
self.isAdmin = personView.isAdmin
isAdmin = personView.isAdmin
}

self.update(with: personView.person)
update(with: personView.person)
}

mutating func update(with person: APIPerson) {
self.person = person

self.userId = person.id
self.name = person.name
self.displayName = person.displayName ?? person.name
self.bio = person.bio
userId = person.id
name = person.name
displayName = person.displayName ?? person.name
bio = person.bio

self.avatar = person.avatarUrl
self.banner = person.bannerUrl
avatar = person.avatarUrl
banner = person.bannerUrl

self.banned = person.banned
self.local = person.local
self.deleted = person.deleted
self.isBot = person.botAccount
banned = person.banned
local = person.local
deleted = person.deleted
isBot = person.botAccount

self.isAdmin = person.admin
isAdmin = person.admin

self.creationDate = person.published
self.updatedDate = person.updated
self.banExpirationDate = person.banExpires
creationDate = person.published
updatedDate = person.updated
banExpirationDate = person.banExpires

self.instanceId = person.instanceId
self.matrixUserId = person.matrixUserId
instanceId = person.instanceId
matrixUserId = person.matrixUserId

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

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

Expand Down Expand Up @@ -181,7 +181,7 @@ struct UserModel {
}
do {
let response = try await personRepository.updateBlocked(for: userId, blocked: blocked)
self.blocked = response.blocked
blocked = response.blocked
RunLoop.main.perform { [self] in
callback(self)
}
Expand All @@ -192,15 +192,15 @@ struct UserModel {
}

static func mock() -> UserModel {
return self.init(from: APIPerson.mock())
self.init(from: APIPerson.mock())
}

var isActiveAccount: Bool {
return siteInformation.myUserInfo?.localUserView.person.id == userId
siteInformation.myUserInfo?.localUserView.person.id == userId
}

var fullyQualifiedUsername: String? {
if let host = self.profileUrl.host() {
if let host = profileUrl.host() {
return "\(name!)@\(host)"
}
return nil
Expand All @@ -227,7 +227,7 @@ extension UserModel: Identifiable {

extension UserModel: Hashable {
static func == (lhs: UserModel, rhs: UserModel) -> Bool {
return lhs.hashValue == rhs.hashValue
lhs.hashValue == rhs.hashValue
}

/// Hashes all fields for which state changes should trigger view updates.
Expand Down
1 change: 0 additions & 1 deletion Mlem/Models/Trackers/SiteInformationTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ class SiteInformationTracker: ObservableObject {
@Published var myUserInfo: APIMyUserInfo?

func load(account: SavedAccount) {

version = account.siteVersion
Task {
do {
Expand Down
2 changes: 1 addition & 1 deletion Mlem/Repositories/PersonRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class PersonRepository {
func loadUserDetails(for url: URL, limit: Int, savedOnly: Bool = false) async throws -> GetPersonDetailsResponse {
let result = try await apiClient.resolve(query: url.absoluteString)
switch result {
case .person(let person):
case let .person(person):
return try await loadUserDetails(for: person.person.id, limit: limit, savedOnly: savedOnly)
default:
throw PersonRequestError.notFound
Expand Down
Loading