diff --git a/Components/Sources/Components/Components/Suggested Edits/Image Recommendations/WKImageRecommendationsView.swift b/Components/Sources/Components/Components/Suggested Edits/Image Recommendations/WKImageRecommendationsView.swift index b05b6b9b73b..8eeb25babd0 100644 --- a/Components/Sources/Components/Components/Suggested Edits/Image Recommendations/WKImageRecommendationsView.swift +++ b/Components/Sources/Components/Components/Suggested Edits/Image Recommendations/WKImageRecommendationsView.swift @@ -7,6 +7,10 @@ struct WKImageRecommendationsView: View { @ObservedObject var appEnvironment = WKAppEnvironment.current @ObservedObject var viewModel: WKImageRecommendationsViewModel let viewArticleAction: (String) -> Void + + var isRTL: Bool { + return viewModel.semanticContentAttribute == .forceRightToLeft + } var sizeClassPadding: CGFloat { horizontalSizeClass == .regular ? 64 : 16 @@ -23,6 +27,7 @@ struct WKImageRecommendationsView: View { VStack { HStack { WKArticleSummaryView(articleSummary: articleSummary) + .environment(\.layoutDirection, isRTL ? .rightToLeft : .leftToRight) } Spacer() .frame(height: 19) diff --git a/Components/Sources/Components/Components/Suggested Edits/Image Recommendations/WKImageRecommendationsViewModel.swift b/Components/Sources/Components/Components/Suggested Edits/Image Recommendations/WKImageRecommendationsViewModel.swift index 0b4dc7a5f43..cda8ff068ac 100644 --- a/Components/Sources/Components/Components/Suggested Edits/Image Recommendations/WKImageRecommendationsViewModel.swift +++ b/Components/Sources/Components/Components/Suggested Edits/Image Recommendations/WKImageRecommendationsViewModel.swift @@ -1,6 +1,7 @@ import Foundation import WKData import Combine +import UIKit public final class WKImageRecommendationsViewModel: ObservableObject { @@ -69,6 +70,7 @@ public final class WKImageRecommendationsViewModel: ObservableObject { // MARK: - Properties let project: WKProject + let semanticContentAttribute: UISemanticContentAttribute let localizedStrings: LocalizedStrings private(set) var imageRecommendations: [ImageRecommendation] = [] @@ -83,8 +85,9 @@ public final class WKImageRecommendationsViewModel: ObservableObject { // MARK: - Lifecycle - public init(project: WKProject, localizedStrings: LocalizedStrings) { + public init(project: WKProject, semanticContentAttribute: UISemanticContentAttribute, localizedStrings: LocalizedStrings) { self.project = project + self.semanticContentAttribute = semanticContentAttribute self.localizedStrings = localizedStrings self.growthTasksDataController = WKGrowthTasksDataController(project: project) self.articleSummaryDataController = WKArticleSummaryDataController() @@ -107,9 +110,7 @@ public final class WKImageRecommendationsViewModel: ObservableObject { } loading = true - growthTasksDataController.getImageRecommendationsCombined { [weak self] result in - guard let self else { completion() return diff --git a/Components/Sources/Components/Style/WKFont.swift b/Components/Sources/Components/Style/WKFont.swift index 27302af0b4b..faf5e22dac7 100644 --- a/Components/Sources/Components/Style/WKFont.swift +++ b/Components/Sources/Components/Style/WKFont.swift @@ -32,7 +32,7 @@ public enum WKFont { case italicsGeorgiaHeadline case boldItalicsGeorgiaHeadline - static func `for`(_ font: WKFont, compatibleWith traitCollection: UITraitCollection = WKAppEnvironment.current.traitCollection) -> UIFont { + public static func `for`(_ font: WKFont, compatibleWith traitCollection: UITraitCollection = WKAppEnvironment.current.traitCollection) -> UIFont { switch font { case .headline: return UIFont.preferredFont(forTextStyle: .headline, compatibleWith: traitCollection) diff --git a/Components/Sources/Components/Style/WKIcon.swift b/Components/Sources/Components/Style/WKIcon.swift index 718a5aae38b..570b8ec803a 100644 --- a/Components/Sources/Components/Style/WKIcon.swift +++ b/Components/Sources/Components/Style/WKIcon.swift @@ -41,6 +41,7 @@ public enum WKSFSymbolIcon { case link case curlybraces case photo + case addPhoto case docTextMagnifyingGlass case magnifyingGlass case listBullet @@ -105,6 +106,8 @@ public enum WKSFSymbolIcon { image = UIImage(systemName: "curlybraces", withConfiguration: configuration) case .photo: image = UIImage(systemName: "photo", withConfiguration: configuration) + case .addPhoto: + image = UIImage(systemName: "photo.badge.plus", withConfiguration: configuration) case .docTextMagnifyingGlass: image = UIImage(systemName: "doc.text.magnifyingglass", withConfiguration: configuration) case .magnifyingGlass: diff --git a/Components/Tests/ComponentsTests/WKImageRecommendationsViewModelTests.swift b/Components/Tests/ComponentsTests/WKImageRecommendationsViewModelTests.swift index 6cade676131..a9dd27fe8c0 100644 --- a/Components/Tests/ComponentsTests/WKImageRecommendationsViewModelTests.swift +++ b/Components/Tests/ComponentsTests/WKImageRecommendationsViewModelTests.swift @@ -14,7 +14,7 @@ final class WKImageRecommendationsViewModelTests: XCTestCase { } func testFetchInitialImageRecommendations() throws { - let viewModel = WKImageRecommendationsViewModel(project: csProject, localizedStrings: localizedStrings) + let viewModel = WKImageRecommendationsViewModel(project: csProject, semanticContentAttribute: .forceLeftToRight, localizedStrings: localizedStrings) let expectation = XCTestExpectation(description: "Fetch Image Recommendations") @@ -30,7 +30,7 @@ final class WKImageRecommendationsViewModelTests: XCTestCase { } func testFetchNextImageRecommendation() throws { - let viewModel = WKImageRecommendationsViewModel(project: csProject, localizedStrings: localizedStrings) + let viewModel = WKImageRecommendationsViewModel(project: csProject, semanticContentAttribute: .forceLeftToRight, localizedStrings: localizedStrings) let expectation1 = XCTestExpectation(description: "Fetch Image Recommendations") diff --git a/WKData/Sources/WKData/Data Controllers/WKGrowthTasks/WKGrowthTasksDataController.swift b/WKData/Sources/WKData/Data Controllers/WKGrowthTasks/WKGrowthTasksDataController.swift index 253be0bfd68..980a46e6461 100644 --- a/WKData/Sources/WKData/Data Controllers/WKGrowthTasks/WKGrowthTasksDataController.swift +++ b/WKData/Sources/WKData/Data Controllers/WKGrowthTasks/WKGrowthTasksDataController.swift @@ -1,6 +1,6 @@ import Foundation -public final class WKGrowthTasksDataController { +@objc public final class WKGrowthTasksDataController: NSObject { private var service = WKDataEnvironment.current.mediaWikiService let project: WKProject @@ -72,6 +72,7 @@ public final class WKGrowthTasksDataController { } fileprivate func getGrowthAPIImageSuggestions(for page: WKImageRecommendationAPIResponse.Page) -> [WKImageRecommendation.GrowthImageSuggestionData] { + var suggestions: [WKImageRecommendation.GrowthImageSuggestionData] = [] for item in page.growthimagesuggestiondata ?? [] { @@ -115,3 +116,25 @@ public final class WKGrowthTasksDataController { public enum WKGrowthTaskType: String { case imageRecommendation = "image-recommendation" } + +// MARK: Objective-C Helpers + +public extension WKGrowthTasksDataController { + + @objc convenience init(languageCode: String) { + let language = WKLanguage(languageCode: languageCode, languageVariantCode: nil) + self.init(project: WKProject.wikipedia(language)) + } + + @objc func hasImageRecommendations(completion: @escaping (Bool) -> Void) { + getImageRecommendationsCombined { result in + switch result { + case .success(let pages): + let pagesWithSuggestions = pages.filter { !($0.growthimagesuggestiondata ?? []).isEmpty } + completion(!pagesWithSuggestions.isEmpty) + case .failure: + completion(false) + } + } + } +} diff --git a/WMF Framework/WMF.h b/WMF Framework/WMF.h index 59c8387c42b..c82d1c6c3d7 100644 --- a/WMF Framework/WMF.h +++ b/WMF Framework/WMF.h @@ -85,6 +85,7 @@ FOUNDATION_EXPORT const unsigned char WMFVersionString[]; #import #import #import +#import #import #import diff --git a/WMF Framework/WMFContentGroup+Extensions.m b/WMF Framework/WMFContentGroup+Extensions.m index f0e5b4c6041..049ecc8ede4 100644 --- a/WMF Framework/WMFContentGroup+Extensions.m +++ b/WMF Framework/WMFContentGroup+Extensions.m @@ -175,35 +175,32 @@ - (void)updateDailySortPriorityWithSortOrderByContentLanguageCode:(nullable NSDi case WMFContentGroupKindFeaturedArticle: updatedDailySortPriority = contentLanguageSortOrder + 4; break; - case WMFContentGroupKindSuggestedEdits: - updatedDailySortPriority = contentLanguageSortOrder + 5; - break; case WMFContentGroupKindTopRead: - updatedDailySortPriority = contentLanguageSortOrder + 6; + updatedDailySortPriority = contentLanguageSortOrder + 5; break; case WMFContentGroupKindNews: - updatedDailySortPriority = contentLanguageSortOrder + 7; + updatedDailySortPriority = contentLanguageSortOrder + 6; break; case WMFContentGroupKindNotification: updatedDailySortPriority = -1; break; case WMFContentGroupKindPictureOfTheDay: - updatedDailySortPriority = 9; + updatedDailySortPriority = 8; break; case WMFContentGroupKindOnThisDay: - updatedDailySortPriority = contentLanguageSortOrder + 10; + updatedDailySortPriority = contentLanguageSortOrder + 9; break; case WMFContentGroupKindLocationPlaceholder: - updatedDailySortPriority = contentLanguageSortOrder + 11; + updatedDailySortPriority = contentLanguageSortOrder + 10; break; case WMFContentGroupKindLocation: - updatedDailySortPriority = contentLanguageSortOrder + 12; + updatedDailySortPriority = contentLanguageSortOrder + 11; break; case WMFContentGroupKindRandom: - updatedDailySortPriority = contentLanguageSortOrder + 13; + updatedDailySortPriority = contentLanguageSortOrder + 12; break; case WMFContentGroupKindMainPage: - updatedDailySortPriority = contentLanguageSortOrder + 14; + updatedDailySortPriority = contentLanguageSortOrder + 13; break; default: break; diff --git a/WMF Framework/WMFExploreFeedContentController.h b/WMF Framework/WMFExploreFeedContentController.h index 05c0e5ec14a..07bf6fd8167 100644 --- a/WMF Framework/WMFExploreFeedContentController.h +++ b/WMF Framework/WMFExploreFeedContentController.h @@ -37,6 +37,8 @@ extern const NSInteger WMFExploreFeedMaximumNumberOfDays; - (void)updateContentSource:(Class)class force:(BOOL)force completion:(nullable dispatch_block_t)completion; +- (NSArray *)exploreFeedSortDescriptors; + // Preferences /** diff --git a/WMF Framework/WMFExploreFeedContentController.m b/WMF Framework/WMFExploreFeedContentController.m index d9cc3c5bca6..537f92dceaa 100644 --- a/WMF Framework/WMFExploreFeedContentController.m +++ b/WMF Framework/WMFExploreFeedContentController.m @@ -95,6 +95,13 @@ - (void)setExploreFeedPreferences:(NSDictionary *)exploreFeedPreferences { return [self.dataStore.languageLinkController.preferredSiteURLs copy]; } +- (NSArray *)exploreFeedSortDescriptors { + NSSortDescriptor *midnightUTCDateSort = [[NSSortDescriptor alloc] initWithKey:@"midnightUTCDate" ascending:NO]; + NSSortDescriptor *dailySort = [[NSSortDescriptor alloc] initWithKey:@"dailySortPriority" ascending:YES]; + NSSortDescriptor *dateSort = [[NSSortDescriptor alloc] initWithKey:@"date" ascending:NO]; + return @[midnightUTCDateSort, dailySort, dateSort]; +} + #pragma mark - Content Sources - (WMFFeedContentSource *)feedContentSource { @@ -699,7 +706,7 @@ - (void)applyExploreFeedPreferencesToAllObjectsInManagedObjectContext:(NSManaged [self applyExploreFeedPreferencesToObjects:contentGroups inManagedObjectContext:moc]; } -- (void)applyExploreFeedPreferencesToObjects:(id)objects inManagedObjectContext:(NSManagedObjectContext *)moc { +- (void)applyExploreFeedPreferencesToObjects:(NSArray*)objects inManagedObjectContext:(NSManagedObjectContext *)moc { NSDictionary *exploreFeedPreferences = [self exploreFeedPreferencesInManagedObjectContext:moc]; for (NSManagedObject *object in objects) { if (![object isKindOfClass:[WMFContentGroup class]]) { @@ -740,6 +747,24 @@ - (void)applyExploreFeedPreferencesToObjects:(id)objects inMa contentGroup.isVisible = isVisible; } } + + // Do a second pass over objects and shuffle ordering around + + NSArray *nonSuggestedEditsGroups = [[objects filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"contentGroupKindInteger != %d && isVisible == true", WMFContentGroupKindSuggestedEdits]] sortedArrayUsingDescriptors:self.exploreFeedSortDescriptors]; + + WMFContentGroup *suggestedEditsGroup = [objects filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"contentGroupKindInteger == %d && isVisible == true", WMFContentGroupKindSuggestedEdits]].firstObject; + + NSMutableArray *finalSorting = [[NSMutableArray alloc] initWithArray:nonSuggestedEditsGroups]; + if (suggestedEditsGroup && finalSorting.count > 0) { + // Suggested Edits should always be 2nd card in the feed. + [finalSorting insertObject:suggestedEditsGroup atIndex:1]; + } + + int index = 0; + for(WMFContentGroup *group in finalSorting) { + group.dailySortPriority = index; + index++; + } } - (void)save:(NSManagedObjectContext *)moc { diff --git a/Wikipedia.xcodeproj/project.pbxproj b/Wikipedia.xcodeproj/project.pbxproj index 151bb437751..c6682523274 100644 --- a/Wikipedia.xcodeproj/project.pbxproj +++ b/Wikipedia.xcodeproj/project.pbxproj @@ -509,7 +509,7 @@ 675175DE276D3B9700CD2974 /* DisappearingCallbackNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 675175DB276D3B9700CD2974 /* DisappearingCallbackNavigationController.swift */; }; 67540CA924D221E3008B2894 /* LocationManagerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67540CA824D221E3008B2894 /* LocationManagerFactory.swift */; }; 675A3D6B2A21600100F9B653 /* MediaWikiFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 675A3D6A2A21600100F9B653 /* MediaWikiFetcher.swift */; }; - 675D875A2B8EA16D007D63F8 /* WMFSuggestedEditsContentSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 675D87582B8EA16D007D63F8 /* WMFSuggestedEditsContentSource.h */; }; + 675D875A2B8EA16D007D63F8 /* WMFSuggestedEditsContentSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 675D87582B8EA16D007D63F8 /* WMFSuggestedEditsContentSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; 675D875B2B8EA16D007D63F8 /* WMFSuggestedEditsContentSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 675D87592B8EA16D007D63F8 /* WMFSuggestedEditsContentSource.m */; }; 675D875D2B8FA0FA007D63F8 /* SuggestedEditsExploreCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 675D875C2B8FA0FA007D63F8 /* SuggestedEditsExploreCell.swift */; }; 675D875E2B8FA0FA007D63F8 /* SuggestedEditsExploreCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 675D875C2B8FA0FA007D63F8 /* SuggestedEditsExploreCell.swift */; }; diff --git a/Wikipedia/Code/ExploreCardViewController.swift b/Wikipedia/Code/ExploreCardViewController.swift index 6abee5a7b24..200776ad3c9 100644 --- a/Wikipedia/Code/ExploreCardViewController.swift +++ b/Wikipedia/Code/ExploreCardViewController.swift @@ -385,8 +385,11 @@ class ExploreCardViewController: UIViewController, UICollectionViewDataSource, U return } - // TODO: Temporary UI - cell.caption = "Testing!" + let languageCode = dataStore.languageLinkController.appLanguage?.languageCode + + cell.title = WMFLocalizedString("explore-suggested-edits-image-recs-title", languageCode: languageCode, value: "Add an image", comment: "Title text shown in the image recommendations explore feed card.") + cell.body = WMFLocalizedString("explore-suggested-edits-image-recs-body", languageCode: languageCode, value: "Add suggested images to Wikipedia articles to enhance understanding.", comment: "Body text shown in the image recommendations explore feed card.") + cell.apply(theme: theme) } func updateLocationCells() { diff --git a/Wikipedia/Code/ExploreViewController.swift b/Wikipedia/Code/ExploreViewController.swift index bb36f877deb..df70c4449b2 100644 --- a/Wikipedia/Code/ExploreViewController.swift +++ b/Wikipedia/Code/ExploreViewController.swift @@ -329,7 +329,7 @@ class ExploreViewController: ColumnarCollectionViewController, ExploreCardViewCo let today = NSDate().wmf_midnightUTCDateFromLocal as Date let oldestDate = Calendar.current.date(byAdding: .day, value: -WMFExploreFeedMaximumNumberOfDays, to: today) ?? today fetchRequest.predicate = NSPredicate(format: "isVisible == YES && (placement == NULL || placement == %@) && midnightUTCDate >= %@", "feed", oldestDate as NSDate) - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "midnightUTCDate", ascending: false), NSSortDescriptor(key: "dailySortPriority", ascending: true), NSSortDescriptor(key: "date", ascending: false)] + fetchRequest.sortDescriptors = dataStore.feedContentController.exploreFeedSortDescriptors() let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: dataStore.viewContext, sectionNameKeyPath: "midnightUTCDate", cacheName: nil) fetchedResultsController = frc let updater = CollectionViewUpdater(fetchedResultsController: frc, collectionView: collectionView) @@ -667,7 +667,7 @@ class ExploreViewController: ColumnarCollectionViewController, ExploreCardViewCo func exploreCardViewController(_ exploreCardViewController: ExploreCardViewController, didSelectItemAtIndexPath indexPath: IndexPath) { guard let contentGroup = exploreCardViewController.contentGroup, - let vc = contentGroup.detailViewControllerForPreviewItemAtIndex(indexPath.row, dataStore: dataStore, theme: theme) else { + let vc = contentGroup.detailViewControllerForPreviewItemAtIndex(indexPath.row, dataStore: dataStore, theme: theme, imageRecDelegate: self) else { return } diff --git a/Wikipedia/Code/SuggestedEditsExploreCell.swift b/Wikipedia/Code/SuggestedEditsExploreCell.swift index 08fd6bcd00c..61c949e8c9f 100644 --- a/Wikipedia/Code/SuggestedEditsExploreCell.swift +++ b/Wikipedia/Code/SuggestedEditsExploreCell.swift @@ -1,58 +1,112 @@ import UIKit +import Components class SuggestedEditsExploreCell: CollectionViewCell { - // TODO: Temporary UI - private let captionLabel: UILabel = UILabel() + private let titleLabel: UILabel = UILabel() + private let bodyLabel: UILabel = UILabel() + private let imageView: UIImageView? = { + return UIImageView(image: WKSFSymbolIcon.for(symbol: .addPhoto, font: WKFont.title1)) + }() - var caption: String? { + var title: String? { get { - return captionLabel.text + return titleLabel.text } set { - captionLabel.text = newValue + titleLabel.text = newValue setNeedsLayout() } } + var body: String? { + get { + return bodyLabel.text + } + set { + bodyLabel.text = newValue + setNeedsLayout() + } + } + + private var isRTL: Bool { + return appLangSemanticContentAttribute == .forceRightToLeft + } + + private var appLangSemanticContentAttribute: UISemanticContentAttribute { + + if let contentLanguageCode = MWKDataStore.shared().languageLinkController.appLanguage?.contentLanguageCode { + let semanticContentAttribute = MWKLanguageLinkController.semanticContentAttribute(forContentLanguageCode: contentLanguageCode) + return semanticContentAttribute + } + + return semanticContentAttribute + } + override func setup() { super.setup() - captionLabel.numberOfLines = 3 - addSubview(captionLabel) + titleLabel.numberOfLines = 0 + titleLabel.textAlignment = isRTL ? .right : .left + bodyLabel.numberOfLines = 0 + bodyLabel.textAlignment = isRTL ? .right : .left + addSubview(titleLabel) + addSubview(bodyLabel) + + if let imageView { + imageView.contentMode = .scaleAspectFit + addSubview(imageView) + } } override func updateFonts(with traitCollection: UITraitCollection) { super.updateFonts(with: traitCollection) - captionLabel.font = UIFont.wmf_font(.subheadline, compatibleWithTraitCollection: traitCollection) - } - - override func reset() { - super.reset() - captionLabel.text = nil + titleLabel.font = WKFont.for(.callout, compatibleWith: traitCollection) + bodyLabel.font = WKFont.for(.subheadline, compatibleWith: traitCollection) } override func sizeThatFits(_ size: CGSize, apply: Bool) -> CGSize { - // TODO: maybe set layoutMarginsAdditions + layoutMarginsAdditions = UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0) let layoutMargins = calculatedLayoutMargins + let maxImageWidth = CGFloat(100) - let widthToFit = size.width - layoutMargins.right - layoutMargins.left - - let origin = CGPoint(x: layoutMargins.left, y: layoutMargins.top) + let initialOrigin = CGPoint(x: layoutMargins.left, y: layoutMargins.top) + + let imageSize = imageView?.wmf_preferredFrame(at: initialOrigin, maximumWidth: maxImageWidth, alignedBy: .forceLeftToRight, apply: false).size ?? .zero + + let labelsImageSpacing = CGFloat(16) + + let labelWidthToFit = size.width - layoutMargins.right - layoutMargins.left - imageSize.width - labelsImageSpacing + + let titleWidth = titleLabel.wmf_preferredFrame(at: initialOrigin, maximumSize: CGSize(width: labelWidthToFit, height: UIView.noIntrinsicMetric), minimumSize: NoIntrinsicSize, alignedBy: .forceLeftToRight, apply: false).size.width + let bodyWidth = bodyLabel.wmf_preferredFrame(at: initialOrigin, maximumSize: CGSize(width: labelWidthToFit, height: UIView.noIntrinsicMetric), minimumSize: NoIntrinsicSize, alignedBy: .forceLeftToRight, apply: false).size.width + + let titleOrigin = isRTL ? CGPoint(x: size.width - layoutMargins.right - titleWidth, y: initialOrigin.y) : initialOrigin - let frame = captionLabel.wmf_preferredFrame(at: origin, maximumSize: CGSize(width: widthToFit, height: UIView.noIntrinsicMetric), minimumSize: NoIntrinsicSize, alignedBy: semanticContentAttribute, apply: apply) + let titleFrame = titleLabel.wmf_preferredFrame(at: titleOrigin, maximumSize: CGSize(width: labelWidthToFit, height: UIView.noIntrinsicMetric), minimumSize: NoIntrinsicSize, alignedBy: .forceLeftToRight, apply: apply) - let finalHeight = frame.maxY + layoutMargins.bottom + let titleBodySpacing = CGFloat(5) + + let bodyOrigin = isRTL ? CGPoint(x: size.width - layoutMargins.right - bodyWidth, y: titleFrame.maxY + titleBodySpacing) : CGPoint(x: initialOrigin.x, y: titleFrame.maxY + titleBodySpacing) + + let bodyFrame = bodyLabel.wmf_preferredFrame(at: bodyOrigin, maximumSize: CGSize(width: labelWidthToFit, height: UIView.noIntrinsicMetric), minimumSize: NoIntrinsicSize, alignedBy: .forceLeftToRight, apply: apply) + + let finalHeight = bodyFrame.maxY + layoutMargins.bottom + + let imageY = (finalHeight / 2) - (imageSize.height / 2) + let imageX = isRTL ? initialOrigin.x : size.width - layoutMargins.right - imageSize.width + + imageView?.wmf_preferredFrame(at: CGPoint(x: imageX, y: imageY), maximumWidth: maxImageWidth, alignedBy: .forceLeftToRight, apply: true) return CGSize(width: size.width, height: finalHeight) } - - } extension SuggestedEditsExploreCell: Themeable { func apply(theme: Theme) { - captionLabel.textColor = theme.colors.primaryText + titleLabel.textColor = theme.colors.primaryText + bodyLabel.textColor = theme.colors.secondaryText + imageView?.tintColor = theme.colors.link } } diff --git a/Wikipedia/Code/WMFAppViewController.m b/Wikipedia/Code/WMFAppViewController.m index b4ddcdebc94..9a06e12a3bf 100644 --- a/Wikipedia/Code/WMFAppViewController.m +++ b/Wikipedia/Code/WMFAppViewController.m @@ -2117,6 +2117,10 @@ - (void)userWasLoggedOut:(NSNotification *)note { force:YES completion:nil]; } + + [self.dataStore.feedContentController updateContentSource:[WMFSuggestedEditsContentSource class] + force:YES + completion:nil]; }); } @@ -2130,6 +2134,10 @@ - (void)userWasLoggedIn:(NSNotification *)note { force:YES completion:nil]; } + + [self.dataStore.feedContentController updateContentSource:[WMFSuggestedEditsContentSource class] + force:YES + completion:nil]; }); } diff --git a/Wikipedia/Code/WMFAuthenticationManager.swift b/Wikipedia/Code/WMFAuthenticationManager.swift index 13dd732d4ac..791819e0594 100644 --- a/Wikipedia/Code/WMFAuthenticationManager.swift +++ b/Wikipedia/Code/WMFAuthenticationManager.swift @@ -63,6 +63,16 @@ import CocoaLumberjackSwift private var isAnonCache: [String: Bool] = [:] private var loggedInUserCache: [String: WMFCurrentlyLoggedInUser] = [:] + @objc func getLoggedInUser(for siteURL: URL, completion: @escaping (WMFCurrentlyLoggedInUser?) -> Void) { + getLoggedInUser(for: siteURL) { result in + switch result { + case .success(let user): + completion(user) + default: + completion(nil) + } + } + } /// Returns the currently logged in user for a given site. Useful to determine the user's groups for a given wiki public func getLoggedInUser(for siteURL: URL, completion: @escaping (Result) -> Void ) { assert(Thread.isMainThread) diff --git a/Wikipedia/Code/WMFContentGroup+DetailViewControllers.swift b/Wikipedia/Code/WMFContentGroup+DetailViewControllers.swift index 2849f2428bd..05853f555e3 100644 --- a/Wikipedia/Code/WMFContentGroup+DetailViewControllers.swift +++ b/Wikipedia/Code/WMFContentGroup+DetailViewControllers.swift @@ -2,8 +2,13 @@ import Foundation import Components extension WMFContentGroup { - @objc(detailViewControllerForPreviewItemAtIndex:dataStore:theme:) + + @objc(detailViewControllerForPreviewItemAtIndex:dataStore:theme:) public func detailViewControllerForPreviewItemAtIndex(_ index: Int, dataStore: MWKDataStore, theme: Theme) -> UIViewController? { + detailViewControllerForPreviewItemAtIndex(index, dataStore: dataStore, theme: theme, imageRecDelegate: nil) + } + + public func detailViewControllerForPreviewItemAtIndex(_ index: Int, dataStore: MWKDataStore, theme: Theme, imageRecDelegate: WKImageRecommendationsDelegate?) -> UIViewController? { switch detailType { case .page: guard let articleURL = previewArticleURLForItemAtIndex(index) else { @@ -22,6 +27,8 @@ extension WMFContentGroup { return WMFPOTDImageGalleryViewController(dates: [date], theme: theme, overlayViewTopBarHidden: false) case .story, .event: return detailViewControllerWithDataStore(dataStore, theme: theme, imageRecDelegate: nil) + case .suggestedEdits: + return detailViewControllerWithDataStore(dataStore, theme: theme, imageRecDelegate: imageRecDelegate) default: return nil } @@ -65,12 +72,15 @@ extension WMFContentGroup { vc = firstRandom case .imageRecommendations: - guard let siteURL = dataStore.languageLinkController.appLanguage?.siteURL, - let project = WikimediaProject(siteURL: siteURL)?.wkProject, + guard let appLanguage = dataStore.languageLinkController.appLanguage, + let project = WikimediaProject(siteURL: appLanguage.siteURL)?.wkProject, let imageRecDelegate = imageRecDelegate else { return nil } + let contentLanguageCode = appLanguage.contentLanguageCode + let semanticContentAttribute = MWKLanguageLinkController.semanticContentAttribute(forContentLanguageCode: contentLanguageCode) + let title = WMFLocalizedString("image-rec-title", value: "Add image", comment: "Title of the image recommendation view. Displayed in the navigation bar above an article summary.") let viewArticle = WMFLocalizedString("image-rec-view-article", value: "View article", comment: "Button from an image recommendation article summary. Tapping the button displays the full article.") @@ -88,7 +98,7 @@ extension WMFContentGroup { let localizedStrings = WKImageRecommendationsViewModel.LocalizedStrings(title: CommonStrings.addImageTitle, viewArticle: CommonStrings.viewArticle, onboardingStrings: onboardingStrings, bottomSheetTitle: CommonStrings.bottomSheetTitle, yesButtonTitle: CommonStrings.yesButtonTitle, noButtonTitle: CommonStrings.noButtonTitle, notSureButtonTitle: CommonStrings.notSureButtonTitle) - let viewModel = WKImageRecommendationsViewModel(project: project, localizedStrings: localizedStrings) + let viewModel = WKImageRecommendationsViewModel(project: project, semanticContentAttribute: semanticContentAttribute, localizedStrings: localizedStrings) let imageRecommendationsViewController = WKImageRecommendationsViewController(viewModel: viewModel, delegate: imageRecDelegate) return imageRecommendationsViewController default: diff --git a/Wikipedia/Code/WMFContentGroup+WMFFeedContentDisplaying.m b/Wikipedia/Code/WMFContentGroup+WMFFeedContentDisplaying.m index 997eefee3b5..b91cc644e94 100644 --- a/Wikipedia/Code/WMFContentGroup+WMFFeedContentDisplaying.m +++ b/Wikipedia/Code/WMFContentGroup+WMFFeedContentDisplaying.m @@ -261,7 +261,7 @@ - (WMFFeedDetailType)detailType { case WMFContentGroupKindReadingList: return WMFFeedDetailTypeNone; case WMFContentGroupKindSuggestedEdits: - return WMFFeedDetailTypeNone; + return WMFFeedDetailTypeSuggestedEdits; case WMFContentGroupKindAnnouncement: return WMFFeedDetailTypeNone; case WMFContentGroupKindUnknown: diff --git a/Wikipedia/Code/WMFCurrentlyLoggedInUserFetcher.swift b/Wikipedia/Code/WMFCurrentlyLoggedInUserFetcher.swift index c41db605b6e..c4f4d68ee65 100644 --- a/Wikipedia/Code/WMFCurrentlyLoggedInUserFetcher.swift +++ b/Wikipedia/Code/WMFCurrentlyLoggedInUserFetcher.swift @@ -20,10 +20,14 @@ public typealias WMFCurrentlyLoggedInUserBlock = (WMFCurrentlyLoggedInUser) -> V @objc public var userID: Int @objc public var name: String @objc public var groups: [String] - init(userID: Int, name: String, groups: [String]) { + @objc public var editCount: UInt64 + @objc public var isBlocked: Bool + init(userID: Int, name: String, groups: [String], editCount: UInt64, isBlocked: Bool) { self.userID = userID self.name = name self.groups = groups + self.editCount = editCount + self.isBlocked = isBlocked } } @@ -32,7 +36,7 @@ public class WMFCurrentlyLoggedInUserFetcher: Fetcher { let parameters = [ "action": "query", "meta": "userinfo", - "uiprop": "groups", + "uiprop": "groups|blockinfo|editcount", "format": "json" ] @@ -54,8 +58,19 @@ public class WMFCurrentlyLoggedInUserFetcher: Fetcher { failure(WMFCurrentlyLoggedInUserFetcherError.userIsAnonymous) return } + + let editCount = userinfo["editcount"] as? UInt64 ?? 0 + + var isBlocked = false + if let blockID = userinfo["blockid"] as? UInt64 { + let blockPartial = (userinfo["blockpartial"] as? Bool ?? false) + if !blockPartial { + isBlocked = true + } + } + let groups = userinfo["groups"] as? [String] ?? [] - success(WMFCurrentlyLoggedInUser.init(userID: userID, name: userName, groups: groups)) + success(WMFCurrentlyLoggedInUser.init(userID: userID, name: userName, groups: groups, editCount: editCount, isBlocked: isBlocked)) } } } diff --git a/Wikipedia/Code/WMFFeedContentDisplaying.h b/Wikipedia/Code/WMFFeedContentDisplaying.h index b028486c7cd..27981c59e71 100644 --- a/Wikipedia/Code/WMFFeedContentDisplaying.h +++ b/Wikipedia/Code/WMFFeedContentDisplaying.h @@ -30,7 +30,8 @@ typedef NS_ENUM(NSUInteger, WMFFeedDetailType) { WMFFeedDetailTypePageWithRandomButton, WMFFeedDetailTypeGallery, WMFFeedDetailTypeStory, - WMFFeedDetailTypeEvent + WMFFeedDetailTypeEvent, + WMFFeedDetailTypeSuggestedEdits }; typedef NS_ENUM(NSUInteger, WMFFeedHeaderType) { diff --git a/Wikipedia/Code/WMFSuggestedEditsContentSource.m b/Wikipedia/Code/WMFSuggestedEditsContentSource.m index e91947c613e..47a8760610d 100644 --- a/Wikipedia/Code/WMFSuggestedEditsContentSource.m +++ b/Wikipedia/Code/WMFSuggestedEditsContentSource.m @@ -1,9 +1,11 @@ #import "WMFSuggestedEditsContentSource.h" #import +@import WKData; @interface WMFSuggestedEditsContentSource () @property (readwrite, nonatomic) MWKDataStore *dataStore; +@property (readwrite, nonatomic, strong) WKGrowthTasksDataController *growthTasksDataController; @end @@ -14,27 +16,59 @@ - (instancetype)initWithDataStore:(MWKDataStore *)dataStore { self = [super init]; if (self) { self.dataStore = dataStore; + NSString *languageCode = dataStore.languageLinkController.appLanguage.languageCode; + self.growthTasksDataController = [[WKGrowthTasksDataController alloc] initWithLanguageCode:languageCode]; } return self; } - (void)loadNewContentInManagedObjectContext:(nonnull NSManagedObjectContext *)moc force:(BOOL)force completion:(nullable dispatch_block_t)completion { - // TODO: Fetch user edit count, image recommendations, user login state, blocked state. + // First delete old card + [self removeAllContentInManagedObjectContext:moc]; - // TODO: if edit count > 50, wiki has image recommendations to review, user is logged in, and user is not blocked (do we need to worry about page protection?) - NSURL *URL = [WMFContentGroup suggestedEditsURL]; - [moc fetchOrCreateGroupForURL:URL - ofKind:WMFContentGroupKindSuggestedEdits - forDate:[NSDate date] - withSiteURL:self.appLanguageSiteURL - associatedContent:nil - customizationBlock:nil]; + NSURL *appLanguageSiteURL = self.dataStore.languageLinkController.appLanguage.siteURL; + + if (!appLanguageSiteURL) { + completion(); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + + WMFTaskGroup *group = [WMFTaskGroup new]; + + __block WMFCurrentlyLoggedInUser *currentUser = nil; + __block BOOL hasImageRecommendations = NO; + + [group enter]; + [self.dataStore.authenticationManager getLoggedInUserFor:appLanguageSiteURL completion:^(WMFCurrentlyLoggedInUser *user) { + currentUser = user; + [group leave]; + }]; + + [group enter]; + [self.growthTasksDataController hasImageRecommendationsWithCompletion:^(BOOL hasRecommendations) { + hasImageRecommendations = hasRecommendations; + [group leave]; + }]; + + [group waitInBackgroundWithCompletion:^{ + if (currentUser) { + if (currentUser.editCount > 50 && !currentUser.isBlocked && hasImageRecommendations) { + + NSURL *URL = [WMFContentGroup suggestedEditsURL]; + + [moc fetchOrCreateGroupForURL:URL ofKind:WMFContentGroupKindSuggestedEdits forDate:[NSDate date] withSiteURL:appLanguageSiteURL associatedContent:nil customizationBlock:nil]; + + } + } + + completion(); + }]; + + }); - completion(); - // else - //[self removeAllContentInManagedObjectContext:moc]; - // end if } - (void)removeAllContentInManagedObjectContext:(nonnull NSManagedObjectContext *)moc { diff --git a/Wikipedia/Localizations/en.lproj/Localizable.strings b/Wikipedia/Localizations/en.lproj/Localizable.strings index f85d60b3ee7..e198f95756f 100644 --- a/Wikipedia/Localizations/en.lproj/Localizable.strings +++ b/Wikipedia/Localizations/en.lproj/Localizable.strings @@ -518,6 +518,8 @@ "explore-random-article-sub-heading-from-wikipedia" = "From Wikipedia"; "explore-randomizer" = "Randomizer"; "explore-suggested-edits-footer" = "Add"; +"explore-suggested-edits-image-recs-body" = "Add suggested images to Wikipedia articles to enhance understanding."; +"explore-suggested-edits-image-recs-title" = "Add an image"; "export-user-data-confirmation-message" = "Sharing your app library includes data about your Reading lists and history, preferences, and Explore feed content. This data file should only be shared with a trusted recipient to use for technical diagnostic purposes."; "export-user-data-confirmation-title" = "Share app library?"; "export-user-data-generic-error" = "There was an error while exporting your data. Please try again later."; diff --git a/Wikipedia/Localizations/qqq.lproj/Localizable.strings b/Wikipedia/Localizations/qqq.lproj/Localizable.strings index 8e607ae5303..81fb7507a08 100644 --- a/Wikipedia/Localizations/qqq.lproj/Localizable.strings +++ b/Wikipedia/Localizations/qqq.lproj/Localizable.strings @@ -518,6 +518,8 @@ "explore-random-article-sub-heading-from-wikipedia" = "Subtext beneath the 'Random article' header when the specific language wikipedia is unknown."; "explore-randomizer" = "Displayed on a button that loads another random article - it's a 'Randomizer'"; "explore-suggested-edits-footer" = "Footer for presenting user option to see list of suggested edits."; +"explore-suggested-edits-image-recs-body" = "Body text shown in the image recommendations explore feed card."; +"explore-suggested-edits-image-recs-title" = "Title text shown in the image recommendations explore feed card."; "export-user-data-confirmation-message" = "Message of confirmation modal after user taps \"Export User Data\" button."; "export-user-data-confirmation-title" = "Title of confirmation modal after user taps \"Export User Data\" button."; "export-user-data-generic-error" = "Error message displayed after user has tried exporting their data and an error occurs."; diff --git a/Wikipedia/iOS Native Localizations/en.lproj/Localizable.strings b/Wikipedia/iOS Native Localizations/en.lproj/Localizable.strings index 0505d45248f..9fea0547874 100644 Binary files a/Wikipedia/iOS Native Localizations/en.lproj/Localizable.strings and b/Wikipedia/iOS Native Localizations/en.lproj/Localizable.strings differ