Skip to content
This repository was archived by the owner on Sep 15, 2025. It is now read-only.

Commit 520e927

Browse files
authored
Merge pull request #102 from wordpress-mobile/feature/stats-fetch-post-details
Add support for fetching detail stats for specific post
2 parents c71c6c1 + 0f98a2c commit 520e927

File tree

5 files changed

+5690
-13
lines changed

5 files changed

+5690
-13
lines changed

WordPressKit.xcodeproj/project.pbxproj

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,17 @@
3232
404057DC221C9FD80060250C /* stats-referrer-data.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057DB221C9FD70060250C /* stats-referrer-data.json */; };
3333
4041405E220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405D220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift */; };
3434
40414060220F9F1F00CF7C5B /* StatsAllTimesInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405F220F9F1F00CF7C5B /* StatsAllTimesInsight.swift */; };
35-
40819778221F00E600A298E4 /* SummaryStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40819777221F00E600A298E4 /* SummaryStatsType.swift */; };
36-
4081977B221F153B00A298E4 /* stats-visits-week.json in Resources */ = {isa = PBXBuildFile; fileRef = 40819779221F153A00A298E4 /* stats-visits-week.json */; };
37-
4081977C221F153B00A298E4 /* stats-visits-day.json in Resources */ = {isa = PBXBuildFile; fileRef = 4081977A221F153A00A298E4 /* stats-visits-day.json */; };
38-
4081977E221F269A00A298E4 /* stats-visits-month.json in Resources */ = {isa = PBXBuildFile; fileRef = 4081977D221F269A00A298E4 /* stats-visits-month.json */; };
3935
4081976F221DDE9B00A298E4 /* PostsStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4081976E221DDE9B00A298E4 /* PostsStatsType.swift */; };
4036
40819771221DFDB700A298E4 /* stats-posts-data.json in Resources */ = {isa = PBXBuildFile; fileRef = 40819770221DFDB600A298E4 /* stats-posts-data.json */; };
4137
40819773221E10C900A298E4 /* PublishedPostsStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40819772221E10C900A298E4 /* PublishedPostsStatsType.swift */; };
4238
40819775221E497D00A298E4 /* stats-published-posts.json in Resources */ = {isa = PBXBuildFile; fileRef = 40819774221E497C00A298E4 /* stats-published-posts.json */; };
39+
40819778221F00E600A298E4 /* SummaryStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40819777221F00E600A298E4 /* SummaryStatsType.swift */; };
40+
4081977B221F153B00A298E4 /* stats-visits-week.json in Resources */ = {isa = PBXBuildFile; fileRef = 40819779221F153A00A298E4 /* stats-visits-week.json */; };
41+
4081977C221F153B00A298E4 /* stats-visits-day.json in Resources */ = {isa = PBXBuildFile; fileRef = 4081977A221F153A00A298E4 /* stats-visits-day.json */; };
42+
4081977E221F269A00A298E4 /* stats-visits-month.json in Resources */ = {isa = PBXBuildFile; fileRef = 4081977D221F269A00A298E4 /* stats-visits-month.json */; };
43+
40819783221F5C8200A298E4 /* StatsPostDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40819782221F5C8200A298E4 /* StatsPostDetails.swift */; };
44+
40819785221F74B200A298E4 /* stats-post-details.json in Resources */ = {isa = PBXBuildFile; fileRef = 40819784221F74B200A298E4 /* stats-post-details.json */; };
45+
408197882220A35000A298E4 /* StatsLastPostInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 408197872220A35000A298E4 /* StatsLastPostInsight.swift */; };
4346
40A71C6E220E1D8E002E3D25 /* StatsServiceRemoteV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */; };
4447
40AB1ADA200FED25009B533D /* PluginDirectoryFeedPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40AB1AD9200FED25009B533D /* PluginDirectoryFeedPage.swift */; };
4548
40E4698B2017C2840030DB5F /* plugin-directory-popular.json in Resources */ = {isa = PBXBuildFile; fileRef = 40E4698A2017C2840030DB5F /* plugin-directory-popular.json */; };
@@ -473,7 +476,6 @@
473476
E6C1E8491EF21FC100D139D9 /* is-passwordless-account-no-account-found.json in Resources */ = {isa = PBXBuildFile; fileRef = E6C1E8471EF21FC100D139D9 /* is-passwordless-account-no-account-found.json */; };
474477
E6C1E84A1EF21FC100D139D9 /* is-passwordless-account-success.json in Resources */ = {isa = PBXBuildFile; fileRef = E6C1E8481EF21FC100D139D9 /* is-passwordless-account-success.json */; };
475478
E6D0EE621F7EF9CE0064D3FC /* AccountServiceRemoteREST+SocialService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D0EE611F7EF9CE0064D3FC /* AccountServiceRemoteREST+SocialService.swift */; };
476-
F96E0643221E15A9008E7D97 /* StatsLastPostInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = F96E0642221E15A9008E7D97 /* StatsLastPostInsight.swift */; };
477479
FF20AD2220B8471A00082398 /* WordPressKit.podspec in Resources */ = {isa = PBXBuildFile; fileRef = FF20AD2120B8471A00082398 /* WordPressKit.podspec */; };
478480
FFE247A720C891D1002DF3A2 /* WordPressComOAuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE247A620C891D1002DF3A2 /* WordPressComOAuthTests.swift */; };
479481
FFE247AF20C891E6002DF3A2 /* WordPressComOAuthWrongPasswordFail.json in Resources */ = {isa = PBXBuildFile; fileRef = FFE247A820C891E5002DF3A2 /* WordPressComOAuthWrongPasswordFail.json */; };
@@ -525,14 +527,17 @@
525527
404057DB221C9FD70060250C /* stats-referrer-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-referrer-data.json"; sourceTree = "<group>"; };
526528
4041405D220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsDotComFollowersInsight.swift; sourceTree = "<group>"; };
527529
4041405F220F9F1F00CF7C5B /* StatsAllTimesInsight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsAllTimesInsight.swift; sourceTree = "<group>"; };
528-
40819777221F00E600A298E4 /* SummaryStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryStatsType.swift; sourceTree = "<group>"; };
529-
40819779221F153A00A298E4 /* stats-visits-week.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-visits-week.json"; sourceTree = "<group>"; };
530-
4081977A221F153A00A298E4 /* stats-visits-day.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-visits-day.json"; sourceTree = "<group>"; };
531-
4081977D221F269A00A298E4 /* stats-visits-month.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-visits-month.json"; sourceTree = "<group>"; };
532530
4081976E221DDE9B00A298E4 /* PostsStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsStatsType.swift; sourceTree = "<group>"; };
533531
40819770221DFDB600A298E4 /* stats-posts-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-posts-data.json"; sourceTree = "<group>"; };
534532
40819772221E10C900A298E4 /* PublishedPostsStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishedPostsStatsType.swift; sourceTree = "<group>"; };
535533
40819774221E497C00A298E4 /* stats-published-posts.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-published-posts.json"; sourceTree = "<group>"; };
534+
40819777221F00E600A298E4 /* SummaryStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryStatsType.swift; sourceTree = "<group>"; };
535+
40819779221F153A00A298E4 /* stats-visits-week.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-visits-week.json"; sourceTree = "<group>"; };
536+
4081977A221F153A00A298E4 /* stats-visits-day.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-visits-day.json"; sourceTree = "<group>"; };
537+
4081977D221F269A00A298E4 /* stats-visits-month.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-visits-month.json"; sourceTree = "<group>"; };
538+
40819782221F5C8200A298E4 /* StatsPostDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsPostDetails.swift; sourceTree = "<group>"; };
539+
40819784221F74B200A298E4 /* stats-post-details.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-post-details.json"; sourceTree = "<group>"; };
540+
408197872220A35000A298E4 /* StatsLastPostInsight.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatsLastPostInsight.swift; sourceTree = "<group>"; };
536541
40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsServiceRemoteV2.swift; sourceTree = "<group>"; };
537542
40AB1AD9200FED25009B533D /* PluginDirectoryFeedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginDirectoryFeedPage.swift; sourceTree = "<group>"; };
538543
40E4698A2017C2840030DB5F /* plugin-directory-popular.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "plugin-directory-popular.json"; sourceTree = "<group>"; };
@@ -980,7 +985,6 @@
980985
E6D0EE611F7EF9CE0064D3FC /* AccountServiceRemoteREST+SocialService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountServiceRemoteREST+SocialService.swift"; sourceTree = "<group>"; };
981986
ED05C8FF3E61D93CE5BA527E /* Pods_WordPressKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WordPressKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
982987
EFF80A6E6EE37118CB1DA158 /* Pods_WordPressKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WordPressKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
983-
F96E0642221E15A9008E7D97 /* StatsLastPostInsight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsLastPostInsight.swift; sourceTree = "<group>"; };
984988
FF20AD2120B8471A00082398 /* WordPressKit.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = WordPressKit.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
985989
FFE247A620C891D1002DF3A2 /* WordPressComOAuthTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordPressComOAuthTests.swift; sourceTree = "<group>"; };
986990
FFE247A820C891E5002DF3A2 /* WordPressComOAuthWrongPasswordFail.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = WordPressComOAuthWrongPasswordFail.json; sourceTree = "<group>"; };
@@ -1048,7 +1052,7 @@
10481052
40414061220F9F2800CF7C5B /* Insights */ = {
10491053
isa = PBXGroup;
10501054
children = (
1051-
F96E0642221E15A9008E7D97 /* StatsLastPostInsight.swift */,
1055+
408197872220A35000A298E4 /* StatsLastPostInsight.swift */,
10521056
4041405D220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift */,
10531057
40E7FEA8220FA4050032834E /* StatsEmailFollowersInsight.swift */,
10541058
4041405F220F9F1F00CF7C5B /* StatsAllTimesInsight.swift */,
@@ -1067,6 +1071,7 @@
10671071
children = (
10681072
404057C3221B30140060250C /* Time-based data */,
10691073
40414061220F9F2800CF7C5B /* Insights */,
1074+
40819782221F5C8200A298E4 /* StatsPostDetails.swift */,
10701075
);
10711076
name = V2;
10721077
sourceTree = "<group>";
@@ -1559,6 +1564,7 @@
15591564
93BD27421EE73384002BB00B /* Mock Data */ = {
15601565
isa = PBXGroup;
15611566
children = (
1567+
40819784221F74B200A298E4 /* stats-post-details.json */,
15621568
4081977A221F153A00A298E4 /* stats-visits-day.json */,
15631569
4081977D221F269A00A298E4 /* stats-visits-month.json */,
15641570
40819779221F153A00A298E4 /* stats-visits-week.json */,
@@ -2017,6 +2023,7 @@
20172023
93BD275C1EE73442002BB00B /* is-available-username-success.json in Resources */,
20182024
93AC8ED81ED32FD000900F5A /* stats-v1.1-top-posts-day.json in Resources */,
20192025
74D67F331F15C3740010C5ED /* site-users-delete-auth-failure.json in Resources */,
2026+
40819785221F74B200A298E4 /* stats-post-details.json in Resources */,
20202027
436D563A2118DE3B00CEAA33 /* supported-countries-success.json in Resources */,
20212028
740B23E71F17FB4200067A2A /* xmlrpc-metaweblog-newpost-success.xml in Resources */,
20222029
74FC6F411F191C1D00112505 /* notifications-last-seen.json in Resources */,
@@ -2286,6 +2293,7 @@
22862293
7430C9B41F1927C50051B8E6 /* RemoteReaderSite.m in Sources */,
22872294
74585B8E1F0D51A100E7E667 /* DomainsServiceRemote.swift in Sources */,
22882295
7430C9A41F1927180051B8E6 /* ReaderPostServiceRemote.m in Sources */,
2296+
408197882220A35000A298E4 /* StatsLastPostInsight.swift in Sources */,
22892297
74E2294E1F1E73FE0085F7F2 /* RemotePublicizeService.swift in Sources */,
22902298
9F3E0BA22087345F009CB5BA /* ServiceRequest.swift in Sources */,
22912299
E1A6605F1FD694ED00BAC339 /* PluginDirectoryEntry.swift in Sources */,
@@ -2344,7 +2352,6 @@
23442352
93F50A381F226B9300B5BEBA /* WordPressComServiceRemote.m in Sources */,
23452353
9F4E52002088E38200424676 /* ObjectValidation.swift in Sources */,
23462354
7430C9B81F1927C50051B8E6 /* RemoteReaderTopic.m in Sources */,
2347-
F96E0643221E15A9008E7D97 /* StatsLastPostInsight.swift in Sources */,
23482355
7403A3021EF0726E00DED7DC /* AccountSettings.swift in Sources */,
23492356
40E7FEA9220FA4060032834E /* StatsEmailFollowersInsight.swift in Sources */,
23502357
404057DA221C9D560060250C /* ReferrerStatsType.swift in Sources */,
@@ -2361,6 +2368,7 @@
23612368
93BD27831EE73944002BB00B /* WordPressRSDParser.swift in Sources */,
23622369
7328420421CD786C00126755 /* WordPressComServiceRemote+SiteCreation.swift in Sources */,
23632370
826016F31F9FA17B00533B6C /* Activity.swift in Sources */,
2371+
40819783221F5C8200A298E4 /* StatsPostDetails.swift in Sources */,
23642372
7397F01A220A072500C723F3 /* ActivityServiceRemote_ApiVersion1_0.swift in Sources */,
23652373
40E7FEB1220FB3B60032834E /* StatsAnnualAndMostPopularTimeInsight.swift in Sources */,
23662374
7E3E7A4A20E443890075D159 /* Scanner+extensions.swift in Sources */,
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
public struct StatsPostDetails {
2+
public let fetchedDate: Date
3+
public let totalViewsCount: Int
4+
5+
public let recentWeeks: [StatsWeeklyBreakdown]
6+
public let dailyAveragesPerMonth: [StatsPostViews]
7+
public let monthlyBreakdown: [StatsPostViews]
8+
public let lastTwoWeeks: [StatsPostViews]
9+
}
10+
11+
public struct StatsWeeklyBreakdown {
12+
public let startDay: DateComponents
13+
public let endDay: DateComponents
14+
15+
public let totalViewsCount: Int
16+
public let averageViewsCount: Int
17+
public let changePercentage: Double
18+
19+
public let days: [StatsPostViews]
20+
}
21+
22+
public struct StatsPostViews {
23+
public let period: StatsPeriodUnit
24+
public let date: DateComponents
25+
public let viewsCount: Int
26+
}
27+
28+
extension StatsPostDetails {
29+
init?(jsonDictionary: [String: AnyObject]) {
30+
guard
31+
let fetchedDateString = jsonDictionary["date"] as? String,
32+
let date = type(of: self).dateFormatter.date(from: fetchedDateString),
33+
let totalViewsCount = jsonDictionary["views"] as? Int,
34+
let monthlyBreakdown = jsonDictionary["years"] as? [String: AnyObject],
35+
let monthlyAverages = jsonDictionary["averages"] as? [String: AnyObject],
36+
let recentWeeks = jsonDictionary["weeks"] as? [[String: AnyObject]],
37+
let data = jsonDictionary["data"] as? [[Any]]
38+
else {
39+
return nil
40+
}
41+
42+
self.fetchedDate = date
43+
self.totalViewsCount = totalViewsCount
44+
45+
// It's very hard to describe the format of this response. I tried to make the parsing
46+
// as nice and readable as possible, but in all honestly it's still pretty nasty.
47+
// If you want to see an example response to see how weird this response is, check out
48+
// `stats-post-details.json`.
49+
self.recentWeeks = StatsPostViews.mapWeeklyBreakdown(jsonDictionary: recentWeeks)
50+
self.monthlyBreakdown = StatsPostViews.mapMonthlyBreakdown(jsonDictionary: monthlyBreakdown)
51+
self.dailyAveragesPerMonth = StatsPostViews.mapMonthlyBreakdown(jsonDictionary: monthlyAverages)
52+
self.lastTwoWeeks = StatsPostViews.mapDailyData(data: Array(data.suffix(14)))
53+
}
54+
55+
static var dateFormatter: DateFormatter {
56+
let df = DateFormatter()
57+
df.locale = Locale(identifier: "en_US_POS")
58+
df.dateFormat = "yyyy-MM-dd"
59+
return df
60+
}
61+
}
62+
63+
extension StatsPostViews {
64+
static func mapMonthlyBreakdown(jsonDictionary: [String: AnyObject]) -> [StatsPostViews] {
65+
return jsonDictionary.flatMap { yearKey, value -> [StatsPostViews] in
66+
guard
67+
let yearInt = Int(yearKey),
68+
let monthsDict = value as? [String: AnyObject],
69+
let months = monthsDict["months"] as? [String: Int]
70+
else {
71+
return []
72+
}
73+
74+
return months.compactMap { monthKey, value in
75+
guard
76+
let month = Int(monthKey)
77+
else {
78+
return nil
79+
}
80+
81+
return StatsPostViews(period: .month,
82+
date: DateComponents(year: yearInt, month: month),
83+
viewsCount: value)
84+
}
85+
}
86+
}
87+
}
88+
89+
extension StatsPostViews {
90+
static func mapWeeklyBreakdown(jsonDictionary: [[String: AnyObject]]) -> [StatsWeeklyBreakdown] {
91+
return jsonDictionary.compactMap {
92+
guard
93+
let totalViews = $0["total"] as? Int,
94+
let averageViews = $0["average"] as? Int,
95+
let days = $0["days"] as? [[String: AnyObject]]
96+
else {
97+
return nil
98+
}
99+
100+
let change = ($0["change"] as? Double) ?? 0.0
101+
102+
let mappedDays: [StatsPostViews] = days.compactMap {
103+
guard
104+
let dayString = $0["day"] as? String,
105+
let date = StatsPostDetails.dateFormatter.date(from: dayString),
106+
let viewsCount = $0["count"] as? Int
107+
else {
108+
return nil
109+
}
110+
111+
return StatsPostViews(period: .day,
112+
date: Calendar.autoupdatingCurrent.dateComponents([.year, .month, .day], from: date),
113+
viewsCount: viewsCount)
114+
}
115+
116+
guard !mappedDays.isEmpty else {
117+
return nil
118+
}
119+
120+
121+
return StatsWeeklyBreakdown(startDay: mappedDays.first!.date,
122+
endDay: mappedDays.last!.date,
123+
totalViewsCount: totalViews,
124+
averageViewsCount: averageViews,
125+
changePercentage: change,
126+
days: mappedDays)
127+
}
128+
129+
}
130+
}
131+
132+
extension StatsPostViews {
133+
static func mapDailyData(data: [[Any]]) -> [StatsPostViews] {
134+
return data.compactMap {
135+
guard
136+
let dateString = $0[0] as? String,
137+
let date = StatsPostDetails.dateFormatter.date(from: dateString),
138+
let viewsCount = $0[1] as? Int
139+
else {
140+
return nil
141+
}
142+
143+
return StatsPostViews(period: .day,
144+
date: Calendar.autoupdatingCurrent.dateComponents([.year, .month, .day], from: date),
145+
viewsCount: viewsCount)
146+
}
147+
}
148+
}

WordPressKit/StatsServiceRemoteV2.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,24 @@ public class StatsServiceRemoteV2: ServiceRemoteWordPressComREST {
103103
completion(nil, error)
104104
})
105105
}
106+
107+
public func getDetails(forPostID postID: Int, completion: @escaping ((StatsPostDetails?, Error?) -> Void)) {
108+
let path = self.path(forEndpoint: "sites/\(siteID)/post/\(postID)/", withVersion: ._1_1)
109+
110+
wordPressComRestApi.GET(path, parameters: [:], success: { (response, _) in
111+
guard
112+
let jsonResponse = response as? [String: AnyObject],
113+
let postDetails = StatsPostDetails(jsonDictionary: jsonResponse)
114+
else {
115+
completion(nil, ResponseError.decodingFailure)
116+
return
117+
}
118+
119+
completion(postDetails, nil)
120+
}, failure: { (error, _) in
121+
completion(nil, error)
122+
})
123+
}
106124
}
107125

108126
// MARK: - StatsLastPostInsight-specific hack

0 commit comments

Comments
 (0)