Skip to content

Commit 18ecb43

Browse files
Implement remove me from collection
Contributes to IOS-730
1 parent e20fc9d commit 18ecb43

8 files changed

Lines changed: 187 additions & 42 deletions

File tree

Mastodon/In Progress New Layout and Datamodel/Common Components/Views/TimelineRowViews/AccountRowView.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ struct AccountRowView: View {
1010
@Environment(MastodonNavigationRouter.self) private var navigator
1111
@Environment(AccountRowViewModel.self) var viewModel
1212
let contentWidth: CGFloat
13-
let isInCollection: Bool
13+
let collectionViewModel: CollectionViewModel?
1414

1515
var body: some View {
1616
VStack(alignment: .gutterAlign, spacing: 0) { // gutterAlign keeps the content properly aligned with the gap between avatar and content
@@ -43,9 +43,12 @@ struct AccountRowView: View {
4343
HStack(spacing: doublePadding) {
4444
AccountStatsView(displayType: .largeStacked, accountMetrics: viewModel.account.metrics, onTapOfMetric: nil)
4545
Spacer()
46-
viewModel.relationshipButton.button(isOpaque: false, isInCollection: isInCollection) {
46+
viewModel.relationshipButton.button(isOpaque: false, isInCollection: collectionViewModel != nil) {
4747
Task {
48-
if isInCollection {
48+
if let collectionViewModel {
49+
if let meItem = collectionViewModel.collection.items.first(where: { $0.account_id == viewModel.account.id }) {
50+
collectionViewModel.doRemoveMe(meItemID: meItem.id, navigator: navigator)
51+
}
4952
} else {
5053
try await viewModel.doRelationshipButtonAction(navigator: navigator, isInCollection: false)
5154
}

Mastodon/In Progress New Layout and Datamodel/Common Components/Views/TimelineRowViews/CollectionRowView.swift

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ struct CollectionRowView: View {
3939
Text(viewModel.collection.name ?? "")
4040
.fontWeight(.semibold)
4141
if let author = viewModel.authorHandle {
42-
Text("by \(author)")
42+
Text(L10nLookup.Scene.Collections.authorLabel(author))
4343
.font(.caption)
4444
.foregroundStyle(.secondary)
4545
}
46-
Text("\(viewModel.collection.itemCount) accounts")
46+
Text(L10nLookup.Scene.Collections.numberOfAccounts(viewModel.itemCount))
4747
.font(.caption)
4848
.foregroundStyle(.secondary)
4949
}
@@ -95,7 +95,17 @@ struct CollectionRowView: View {
9595
private(set) var relationshipViewModel = RelationshipViewModel()
9696
var authorHandle: String?
9797
var authorAccount: MastodonAccount?
98+
private var partialAccounts: [Mastodon.Entity.PartialAccountWithAvatar] = []
9899
var accountAvatarUrls: [URL] = []
100+
var iHaveRemovedMyself = false
101+
102+
var itemCount: Int {
103+
if iHaveRemovedMyself {
104+
return collection.itemCount - 1
105+
} else {
106+
return collection.itemCount
107+
}
108+
}
99109

100110
init(collection: Mastodon.Entity.Collection) {
101111
self.collection = collection
@@ -128,21 +138,25 @@ struct CollectionRowView: View {
128138
MastodonMenuAction.menuButton(systemImageName: nil, text: action.labelText) {
129139
Task {
130140
do {
131-
try await self.doMenuAction(action)
141+
try await self.doMenuAction(action, navigator: navigator)
132142
} catch {
133-
// TODO: implement error handling
143+
navigator.didReceiveError(error)
134144
}
135145
}
136146
}
137147
case .removeMyself:
138-
MastodonMenuAction.menuButton(systemImageName: nil, text: action.labelText) {
139-
Task {
140-
do {
141-
try await self.doMenuAction(action)
142-
} catch {
143-
// TODO: implement error handling
148+
if !self.iHaveRemovedMyself {
149+
MastodonMenuAction.menuButton(systemImageName: nil, text: action.labelText) {
150+
Task {
151+
do {
152+
try await self.doMenuAction(action, navigator: navigator)
153+
} catch {
154+
navigator.didReceiveError(error)
155+
}
144156
}
145157
}
158+
} else {
159+
EmptyView()
146160
}
147161
}
148162
case .relationshipAction(let relAction):
@@ -161,14 +175,15 @@ struct CollectionRowView: View {
161175
}
162176
}
163177

164-
func doMenuAction(_ action: MastodonMenuAction.CollectionMenuAction) async throws {
178+
func doMenuAction(_ action: MastodonMenuAction.CollectionMenuAction, navigator: MastodonNavigationRouter) async throws {
165179
switch action {
166180
case .reportCollection:
167181
// TODO: implement
168182
assertionFailure("reportCollection is not yet implemented")
169183
case .removeMyself:
170-
// TODO: implement
171-
assertionFailure("removeMyself is not yet implemented")
184+
if let meItem = collection.items.first(where: { $0.account_id == AuthenticationServiceProvider.shared.currentActiveUser.value?.userID }) {
185+
doRemoveMe(meItemID: meItem.id, navigator: navigator)
186+
}
172187
}
173188
}
174189

@@ -180,6 +195,45 @@ struct CollectionRowView: View {
180195
authorAccount = updated
181196
authorHandle = "@" + updated.handle
182197
}
198+
199+
func updateAvatarUrls(_ updatedPartialAccounts: [Mastodon.Entity.PartialAccountWithAvatar]?) {
200+
if let updatedPartialAccounts {
201+
partialAccounts = updatedPartialAccounts
202+
}
203+
let firstFourAvatars = collection.items.compactMap({ member -> URL? in
204+
guard !iHaveRemovedMyself || member.account_id != AuthenticationServiceProvider.shared.currentActiveUser.value?.userID else { return nil }
205+
guard let partialAccount = partialAccounts.first(where: { $0.id == member.account_id }) else { return nil }
206+
return partialAccount.avatarURL
207+
}).prefix(4)
208+
accountAvatarUrls = Array(firstFourAvatars)
209+
}
210+
211+
func doRemoveMe(meItemID: Mastodon.Entity.CollectionMember.ID, navigator: MastodonNavigationRouter) {
212+
navigator.activeAlert = .confirmRemoveMeFromCollection(collectionName: collection.name ?? "Collection", didConfirm: { confirmed in
213+
if confirmed {
214+
Task {
215+
do {
216+
try await self.commitRemoveMe(meItemID: meItemID)
217+
} catch {
218+
navigator.didReceiveError(error)
219+
}
220+
}
221+
}
222+
})
223+
}
224+
225+
func commitRemoveMe(meItemID: Mastodon.Entity.CollectionMember.ID) async throws {
226+
guard let authBox = AuthenticationServiceProvider.shared.currentActiveUser.value else { return }
227+
try await APIService.shared.removeFromCollection(collectionId: collection.id, collectionMemberId: meItemID, authenticationBox: authBox)
228+
didFinishRemovingMyself()
229+
}
230+
231+
private func didFinishRemovingMyself() {
232+
withAnimation {
233+
self.iHaveRemovedMyself = true
234+
self.updateAvatarUrls(nil)
235+
}
236+
}
183237
}
184238

185239
extension Mastodon.Entity.PartialAccountWithAvatar {

Mastodon/In Progress New Layout and Datamodel/Timeline/TimelineFeedLoader.swift

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,15 @@ public enum MastodonTimelineType: Equatable {
154154
}
155155
}
156156

157+
public var collectionViewModel: CollectionViewModel? {
158+
switch self {
159+
case .collection(let viewModel):
160+
return viewModel
161+
default:
162+
return nil
163+
}
164+
}
165+
157166
public var canDisplayFilteredNotifications: Bool {
158167
switch self {
159168
case .notifications(.everything), .notifications(.mentions):
@@ -538,27 +547,22 @@ final class TimelineFeedLoader: MastodonFeedLoader<TimelineItem, CacheableTimeli
538547
let account = accountViewModels[collection.accountId]?.account
539548
let viewModel = {
540549
let authorHandle = partialAccounts.first(where: { $0.id == collection.accountId })?.fullHandle ?? "someone@somewhere.social"
541-
let firstFourAvatars = collection.items.compactMap({ member -> URL? in
542-
guard let partialAccount = partialAccounts.first(where: { $0.id == member.account_id }) else { return nil }
543-
return partialAccount.avatarURL
544-
}).prefix(4)
545550

546-
if let existing = collectionViewModels[collection.accountId] {
551+
@MainActor func updateModelWithAuthorAccount(_ model: CollectionViewModel) {
547552
if let account {
548-
existing.updateAuthorAccount(account)
553+
model.updateAuthorAccount(account)
549554
} else {
550-
existing.authorHandle = "@" + authorHandle
555+
model.authorHandle = "@" + authorHandle
551556
}
552-
existing.accountAvatarUrls = Array(firstFourAvatars)
557+
model.updateAvatarUrls(partialAccounts)
558+
}
559+
560+
if let existing = collectionViewModels[collection.accountId] {
561+
updateModelWithAuthorAccount(existing)
553562
return existing
554563
} else {
555564
let model = CollectionViewModel(collection: collection)
556-
if let account {
557-
model.updateAuthorAccount(account)
558-
} else {
559-
model.authorHandle = "@" + authorHandle
560-
}
561-
model.accountAvatarUrls = Array(firstFourAvatars)
565+
updateModelWithAuthorAccount(model)
562566
newCollectionModels[collection.id] = model
563567
return model
564568
}

Mastodon/In Progress New Layout and Datamodel/Timeline/TimelineListViewController.swift

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ class TimelineListViewController: UIHostingController<AnyView>
256256
) { _ in
257257
Task {
258258
do {
259-
try await collectionViewModel.doMenuAction(action)
259+
try await collectionViewModel.doMenuAction(action, navigator: self.navigator)
260260
} catch {
261261
self.navigator.didReceiveError(error)
262262
}
@@ -704,6 +704,7 @@ extension MastodonPostMenuAction {
704704
case confirmMute(username: String, didConfirm: (Bool)->())
705705
case confirmUnmute(username: String, didConfirm: (Bool)->())
706706
case confirmRemoveQuote(username: String, didConfirm: (Bool)->())
707+
case confirmRemoveMeFromCollection(collectionName: String, didConfirm: (Bool)->())
707708
case confirmBlock(username: String, didConfirm: (Bool)->())
708709
case confirmUnblock(username: String, didConfirm: (Bool)->())
709710
case confirmDomainBlock(account: MastodonAccount, didConfirm: (Bool)->())
@@ -733,6 +734,8 @@ extension MastodonPostMenuAction {
733734

734735
case .confirmRemoveQuote:
735736
L10n.Common.Alerts.ConfirmRemoveQuote.title
737+
case .confirmRemoveMeFromCollection(let collectionName, _):
738+
L10nLookup.Scene.Collections.confirmRemoveFromCollectionTitle(collectionName: collectionName)
736739
case .confirmBlock:
737740
L10n.Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.title
738741
case .confirmUnblock:
@@ -769,6 +772,8 @@ extension MastodonPostMenuAction {
769772

770773
case .confirmRemoveQuote:
771774
L10n.Common.Alerts.ConfirmRemoveQuote.message
775+
case .confirmRemoveMeFromCollection:
776+
L10nLookup.Scene.Collections.confirmRemoveFromCollectionMessage
772777
case .confirmDeleteOfPost:
773778
L10n.Common.Alerts.DeletePost.message
774779
case .confirmUnhideFeatureTabBeforeFeaturing(let item, _):
@@ -2318,13 +2323,17 @@ struct TimelineListView: View {
23182323
}
23192324
}
23202325
case .account(let accountViewModel):
2321-
AccountRowView(contentWidth: contentWidth, isInCollection: viewModel.timeline.isCollection)
2322-
.environment(accountViewModel)
2323-
.padding(EdgeInsets(top: standardPadding, leading: doublePadding, bottom: standardPadding, trailing: standardPadding))
2324-
.frame(width: useableWidth)
2325-
.onTapGesture {
2326-
navigator.push(.profile(account: accountViewModel.account._legacyEntity, relationship: nil))
2327-
}
2326+
if let collectionViewModel = viewModel.timeline.collectionViewModel, collectionViewModel.iHaveRemovedMyself, accountViewModel.id == AuthenticationServiceProvider.shared.currentActiveUser.value?.userID {
2327+
EmptyView()
2328+
} else {
2329+
AccountRowView(contentWidth: contentWidth, collectionViewModel: viewModel.timeline.collectionViewModel)
2330+
.environment(accountViewModel)
2331+
.padding(EdgeInsets(top: standardPadding, leading: doublePadding, bottom: standardPadding, trailing: standardPadding))
2332+
.frame(width: useableWidth)
2333+
.onTapGesture {
2334+
navigator.push(.profile(account: accountViewModel.account._legacyEntity, relationship: nil))
2335+
}
2336+
}
23282337
case .collection(let collectionViewModel):
23292338
CollectionRowView(contentWidth: contentWidth)
23302339
.environment(collectionViewModel)
@@ -2569,7 +2578,7 @@ struct TimelineListView: View {
25692578
Text(description)
25702579
}
25712580
let myAccountId = AuthenticationServiceProvider.shared.currentActiveUser.value?.userID
2572-
if let meAsMember = collectionViewModel.collection.items.first(where: { $0.account_id == myAccountId }) {
2581+
if !collectionViewModel.iHaveRemovedMyself, let meAsMember = collectionViewModel.collection.items.first(where: { $0.account_id == myAccountId }) {
25732582
infoPlusActionCalloutView(
25742583
image: Image(systemName: "star"),
25752584
headline: L10nLookup.Scene.Collections.youAreFeaturedInThisCollection,
@@ -2578,11 +2587,11 @@ struct TimelineListView: View {
25782587
buttonText: L10nLookup.Scene.Collections.removeMe,
25792588
buttonColor: Asset.Colors.FigmaToken.bgBrandSoft.swiftUIColor
25802589
) {
2581-
// TODO: implement
2590+
collectionViewModel.doRemoveMe(meItemID: meAsMember.id, navigator: navigator)
25822591
}
25832592
.fixedSize(horizontal: false, vertical: true)
25842593
}
2585-
Text(L10nLookup.Scene.Collections.numberOfAccounts(collectionViewModel.collection.itemCount))
2594+
Text(L10nLookup.Scene.Collections.numberOfAccounts(collectionViewModel.itemCount))
25862595
.fontWeight(.semibold)
25872596
.foregroundStyle(.secondary)
25882597
}
@@ -2656,6 +2665,14 @@ struct TimelineListView: View {
26562665
Text(L10n.Common.Controls.Actions.remove)
26572666
}
26582667

2668+
case .confirmRemoveMeFromCollection(_, let didConfirm):
2669+
cancelButton(didConfirm)
2670+
Button(role: .destructive) {
2671+
didConfirm(true)
2672+
} label: {
2673+
Text(L10nLookup.Scene.Collections.removeMe)
2674+
}
2675+
26592676
case .confirmDeleteOfPost(let didConfirm):
26602677
cancelButton(didConfirm)
26612678
Button(role: .destructive) {

MastodonSDK/Sources/MastodonCore/Service/API/APIService+Collections.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ extension APIService {
1515
) async throws -> Mastodon.Response.Content<Mastodon.Entity.CollectionsList> {
1616
let authorization = authenticationBox.userAuthorization
1717

18-
let response = try await Mastodon.API.Collections.getCollectionsFromAccount(
18+
let response = try await Mastodon.API.Collections.getCollectionsFromAccount(
1919
session: session,
2020
domain: authenticationBox.domain,
2121
accountID: accountID,
@@ -24,4 +24,22 @@ extension APIService {
2424

2525
return response
2626
}
27+
28+
public func removeFromCollection(
29+
collectionId: Mastodon.Entity.Collection.ID,
30+
collectionMemberId: Mastodon.Entity.CollectionMember.ID,
31+
authenticationBox: MastodonAuthenticationBox
32+
) async throws {
33+
let authorization = authenticationBox.userAuthorization
34+
35+
try await Mastodon.API.Collections.removeFromCollection(
36+
session: session,
37+
domain: authenticationBox.domain,
38+
collectionID: collectionId,
39+
itemID: collectionMemberId,
40+
authorization: authorization
41+
)
42+
43+
return
44+
}
2745
}

MastodonSDK/Sources/MastodonLocalization/Resources/L10nLookup.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,14 @@ public extension L10nLookup.Scene {
933933
let result = tr("Localizable-Collections", "Scene.Collections.collectionAuthorAddedYouOnDate", author, dateString)
934934
return result
935935
}
936+
public static func confirmRemoveFromCollectionTitle(collectionName: String) -> String {
937+
let result = tr("Localizable-Collections", "Scene.Collections.confirmRemoveFromCollectionTitle", collectionName)
938+
return result
939+
}
940+
public static let confirmRemoveFromCollectionMessage: String = {
941+
let result = tr("Localizable-Collections", "Scene.Collections.confirmRemoveFromCollectionMessage")
942+
return result
943+
}()
936944
}
937945
}
938946

MastodonSDK/Sources/MastodonLocalization/Resources/Localizable-Collections.xcstrings

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,29 @@
647647
}
648648
}
649649
},
650+
"Scene.Collections.confirmRemoveFromCollectionMessage" : {
651+
"extractionState" : "manual",
652+
"localizations" : {
653+
"en" : {
654+
"stringUnit" : {
655+
"state" : "translated",
656+
"value" : "This action can’t be undone. This user will still be able to add you to other collections, unless you block them."
657+
}
658+
}
659+
}
660+
},
661+
"Scene.Collections.confirmRemoveFromCollectionTitle" : {
662+
"comment" : "confirmation alert title",
663+
"extractionState" : "manual",
664+
"localizations" : {
665+
"en" : {
666+
"stringUnit" : {
667+
"state" : "translated",
668+
"value" : "Remove yourself from “%1$(collectionName)@”?"
669+
}
670+
}
671+
}
672+
},
650673
"Scene.Collections.createCollection" : {
651674
"comment" : "button text",
652675
"extractionState" : "manual",

0 commit comments

Comments
 (0)