diff --git a/Localizations/Localizable.strings b/Localizations/Localizable.strings index 2852c04..8935171 100644 --- a/Localizations/Localizable.strings +++ b/Localizations/Localizable.strings @@ -107,6 +107,7 @@ "compose.visibility-button.accessibility-label-%@" = "Privacy: %@"; "compose-button.accessibility-label.post" = "Compose Post"; "compose-button.accessibility-label.toot" = "Compose Toot"; +"conversation.unread" = "Unread"; "emoji.custom" = "Custom"; "emoji.default-skin-tone" = "Default skin tone"; "emoji.default-skin-tone-button.accessibility-label" = "Select default skin tone"; diff --git a/MastodonAPI/Sources/MastodonAPI/Endpoints/ConversationEndpoint.swift b/MastodonAPI/Sources/MastodonAPI/Endpoints/ConversationEndpoint.swift new file mode 100644 index 0000000..d81bfee --- /dev/null +++ b/MastodonAPI/Sources/MastodonAPI/Endpoints/ConversationEndpoint.swift @@ -0,0 +1,31 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import HTTP +import Mastodon + +public enum ConversationEndpoint { + case read(id: Conversation.Id) +} + +extension ConversationEndpoint: Endpoint { + public typealias ResultType = Conversation + + public var context: [String] { + defaultContext + ["conversations"] + } + + public var pathComponentsInContext: [String] { + switch self { + case let .read(id): + return [id, "read"] + } + } + + public var method: HTTPMethod { + switch self { + case .read: + return .post + } + } +} diff --git a/ServiceLayer/Sources/ServiceLayer/Services/ConversationsService.swift b/ServiceLayer/Sources/ServiceLayer/Services/ConversationsService.swift index f2c3cb7..35c0bd7 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/ConversationsService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/ConversationsService.swift @@ -26,6 +26,14 @@ public struct ConversationsService { } } +public extension ConversationsService { + func markConversationAsRead(id: Conversation.Id) -> AnyPublisher { + mastodonAPIClient.request(ConversationEndpoint.read(id: id)) + .flatMap { contentDatabase.insert(conversations: [$0]) } + .eraseToAnyPublisher() + } +} + extension ConversationsService: CollectionService { public func request(maxId: String?, minId: String?, search: Search?) -> AnyPublisher { mastodonAPIClient.pagedRequest(ConversationsEndpoint.conversations, maxId: maxId, minId: minId) diff --git a/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift b/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift index 7e95cb3..752cc53 100644 --- a/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift @@ -149,6 +149,10 @@ extension CollectionItemsViewModel: CollectionViewModel { case let .conversation(conversation): guard let status = conversation.lastStatus else { break } + (collectionService as? ConversationsService)?.markConversationAsRead(id: conversation.id) + .sink { _ in } receiveValue: { _ in } + .store(in: &cancellables) + send(event: .navigation(.collection(collectionService .navigationService .contextService(id: status.displayStatus.id)))) diff --git a/ViewModels/Sources/ViewModels/View Models/ConversationViewModel.swift b/ViewModels/Sources/ViewModels/View Models/ConversationViewModel.swift index 16f4c93..b6a8704 100644 --- a/ViewModels/Sources/ViewModels/View Models/ConversationViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/ConversationViewModel.swift @@ -33,3 +33,7 @@ public final class ConversationViewModel: ObservableObject { self.identityContext = identityContext } } + +public extension ConversationViewModel { + var isUnread: Bool { conversationService.conversation.unread } +} diff --git a/Views/UIKit/Content Views/ConversationView.swift b/Views/UIKit/Content Views/ConversationView.swift index 9bdbb96..f311fb4 100644 --- a/Views/UIKit/Content Views/ConversationView.swift +++ b/Views/UIKit/Content Views/ConversationView.swift @@ -7,6 +7,9 @@ import ViewModels final class ConversationView: UIView { let avatarsView = ConversationAvatarsView() let displayNamesLabel = UILabel() + let unreadIndicator = UIImageView(image: UIImage( + systemName: "circlebadge.fill", + withConfiguration: UIImage.SymbolConfiguration(scale: .small))) let timeLabel = UILabel() let statusBodyView = StatusBodyView() @@ -78,6 +81,7 @@ private extension ConversationView { namesTimeStackView.spacing = .compactSpacing namesTimeStackView.alignment = .top namesTimeStackView.addArrangedSubview(displayNamesLabel) + namesTimeStackView.addArrangedSubview(unreadIndicator) namesTimeStackView.addArrangedSubview(timeLabel) mainStackView.axis = .vertical @@ -90,6 +94,9 @@ private extension ConversationView { displayNamesLabel.adjustsFontForContentSizeCategory = true displayNamesLabel.numberOfLines = 0 + unreadIndicator.contentMode = .scaleAspectFit + unreadIndicator.setContentHuggingPriority(.required, for: .horizontal) + timeLabel.font = .preferredFont(forTextStyle: .subheadline) timeLabel.adjustsFontForContentSizeCategory = true timeLabel.textColor = .secondaryLabel @@ -126,6 +133,7 @@ private extension ConversationView { view: displayNamesLabel) mutableDisplayNames.resizeAttachments(toLineHeight: displayNamesLabel.font.lineHeight) + unreadIndicator.isHidden = !viewModel.isUnread displayNamesLabel.attributedText = mutableDisplayNames timeLabel.text = viewModel.statusViewModel?.time timeLabel.accessibilityLabel = viewModel.statusViewModel?.accessibilityTime @@ -134,6 +142,10 @@ private extension ConversationView { let accessibilityAttributedLabel = NSMutableAttributedString(attributedString: mutableDisplayNames) + if viewModel.isUnread { + accessibilityAttributedLabel.appendWithSeparator(NSLocalizedString("conversation.unread", comment: "")) + } + if let statusBodyAccessibilityAttributedLabel = statusBodyView.accessibilityAttributedLabel { accessibilityAttributedLabel.appendWithSeparator(statusBodyAccessibilityAttributedLabel) }