Merge pull request #993 from mastodon/ios-37-hashtag-widget
Hashtag-Widget (IOS-152)
This commit is contained in:
commit
96e9d8e5ad
|
@ -870,6 +870,22 @@
|
|||
"configuration_description": "Show latest followers.",
|
||||
"title": "Latest followers",
|
||||
"last_update": "Last update: %s"
|
||||
},
|
||||
"hashtag": {
|
||||
"configuration": {
|
||||
"display_name": "Hashtag",
|
||||
"description": "Shows a recent post with the selected hashtag."
|
||||
},
|
||||
"not_found": {
|
||||
"account_name": "John Mastodon",
|
||||
"account": "@johnMastodon@no-such.account",
|
||||
"content": "Sorry, we couldn’t find any posts with the hashtag <a>#%@</a>. Please try a <a>#DifferentHashtag</a> or check the widget settings."
|
||||
},
|
||||
"placeholder": {
|
||||
"account_name": "John Mastodon",
|
||||
"account": "@johnMastodon@no-such.account",
|
||||
"content": "This is how a post with a <a>#hashtag</a> would look. Pick whichever <a>#hashtag</a> you want in the widget settings."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -150,6 +150,9 @@
|
|||
D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */; };
|
||||
D8E5C346296DAB84007E76A7 /* DataSourceFacade+Status+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */; };
|
||||
D8E5C349296DB8A3007E76A7 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */; };
|
||||
D8F0372C29D232730027DE2E /* HashtagIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */; };
|
||||
D8F8A03A29CA5C15000195DD /* HashtagWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F8A03929CA5C15000195DD /* HashtagWidgetView.swift */; };
|
||||
D8F8A03C29CA5CB6000195DD /* HashtagWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F8A03B29CA5CB6000195DD /* HashtagWidget.swift */; };
|
||||
DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */; settings = {ATTRIBUTES = (codegen, ); }; };
|
||||
DB0009A726AEE5DC009B9D2D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */; };
|
||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
|
||||
|
@ -794,6 +797,9 @@
|
|||
D8A6FE6629325F5900666A47 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status+History.swift"; sourceTree = "<group>"; };
|
||||
D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = "<group>"; };
|
||||
D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagIntentHandler.swift; sourceTree = "<group>"; };
|
||||
D8F8A03929CA5C15000195DD /* HashtagWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagWidgetView.swift; sourceTree = "<group>"; };
|
||||
D8F8A03B29CA5CB6000195DD /* HashtagWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagWidget.swift; sourceTree = "<group>"; };
|
||||
DB0009A826AEE5DC009B9D2D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = "<group>"; };
|
||||
DB0009AD26AEE5E4009B9D2D /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Intents.strings; sourceTree = "<group>"; };
|
||||
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
|
||||
|
@ -1461,6 +1467,7 @@
|
|||
2A86A14329892700007F1062 /* Variants */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8F8A03829CA5C02000195DD /* Hashtag */,
|
||||
2A86A14429892709007F1062 /* FollowersCount */,
|
||||
2A86A14729892B1B007F1062 /* MultiFollowersCount */,
|
||||
2A9D0662298C045000BF38CB /* LatestFollowers */,
|
||||
|
@ -1833,6 +1840,15 @@
|
|||
path = "Edit History";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8F8A03829CA5C02000195DD /* Hashtag */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8F8A03929CA5C15000195DD /* HashtagWidgetView.swift */,
|
||||
D8F8A03B29CA5CB6000195DD /* HashtagWidget.swift */,
|
||||
);
|
||||
path = Hashtag;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB01409B25C40BB600F9F3CF /* Onboarding */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2044,6 +2060,7 @@
|
|||
DB98335F25C93B0400AD9700 /* Recovered References */,
|
||||
D8A6FE6029325F5900666A47 /* Localization */,
|
||||
);
|
||||
indentWidth = 4;
|
||||
sourceTree = "<group>";
|
||||
tabWidth = 4;
|
||||
};
|
||||
|
@ -2326,6 +2343,7 @@
|
|||
DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */,
|
||||
2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */,
|
||||
2A86A14529892944007F1062 /* MultiFollowersCountIntentHandler.swift */,
|
||||
D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */,
|
||||
);
|
||||
path = Handler;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3510,7 +3528,9 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D8F8A03A29CA5C15000195DD /* HashtagWidgetView.swift in Sources */,
|
||||
2A33062D2987DBFA001D4C51 /* FollowersCountHistory.swift in Sources */,
|
||||
D8F8A03C29CA5CB6000195DD /* HashtagWidget.swift in Sources */,
|
||||
2A9D0666298C05A800BF38CB /* LatestFollowersWidgetView.swift in Sources */,
|
||||
2AB5011E299243FB00346092 /* WidgetExtension.intentdefinition in Sources */,
|
||||
2A86A14B2989326E007F1062 /* MultiFollowersCountWidgetView.swift in Sources */,
|
||||
|
@ -3962,6 +3982,7 @@
|
|||
2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */,
|
||||
DB64BA452851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift in Sources */,
|
||||
DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */,
|
||||
D8F0372C29D232730027DE2E /* HashtagIntentHandler.swift in Sources */,
|
||||
2A86A14629892944007F1062 /* MultiFollowersCountIntentHandler.swift in Sources */,
|
||||
DB8FABCA26AEC7B2008E5AF4 /* IntentHandler.swift in Sources */,
|
||||
);
|
||||
|
|
|
@ -62,8 +62,9 @@
|
|||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>FollowersCountIntent</string>
|
||||
<string>MultiFollowersCountIntent</string>
|
||||
<string>HashtagIntent</string>
|
||||
<string>LatestFollowersIntent</string>
|
||||
<string>MultiFollowersCountIntent</string>
|
||||
<string>SendPostIntent</string>
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Intents
|
||||
import MastodonSDK
|
||||
|
||||
class HashtagIntentHandler: INExtension, HashtagIntentHandling {
|
||||
func provideHashtagOptionsCollection(for intent: HashtagIntent, searchTerm: String?) async throws -> INObjectCollection<NSString> {
|
||||
|
||||
guard let authenticationBox = WidgetExtension.appContext
|
||||
.authenticationService
|
||||
.mastodonAuthenticationBoxes
|
||||
.first
|
||||
else {
|
||||
return INObjectCollection(items: [])
|
||||
}
|
||||
|
||||
var results: [NSString] = []
|
||||
|
||||
if let searchTerm, searchTerm.isEmpty == false {
|
||||
let searchResults = try await WidgetExtension.appContext
|
||||
.apiService
|
||||
.search(query: .init(q: searchTerm, type: .hashtags), authenticationBox: authenticationBox)
|
||||
.value
|
||||
.hashtags
|
||||
.compactMap { $0.name as NSString }
|
||||
|
||||
results = searchResults
|
||||
|
||||
} else {
|
||||
let followedTags = try await WidgetExtension.appContext.apiService.getFollowedTags(
|
||||
domain: authenticationBox.domain,
|
||||
query: Mastodon.API.Account.FollowedTagsQuery(limit: nil),
|
||||
authenticationBox: authenticationBox)
|
||||
.value
|
||||
.compactMap { $0.name as NSString }
|
||||
|
||||
results = followedTags
|
||||
|
||||
}
|
||||
|
||||
return INObjectCollection(items: results)
|
||||
|
||||
}
|
||||
}
|
|
@ -31,6 +31,8 @@
|
|||
<key>IntentsSupported</key>
|
||||
<array>
|
||||
<string>FollowersCountIntent</string>
|
||||
<string>HashtagIntent</string>
|
||||
<string>LatestFollowersIntent</string>
|
||||
<string>MultiFollowersCountIntent</string>
|
||||
<string>SendPostIntent</string>
|
||||
</array>
|
||||
|
|
|
@ -19,6 +19,8 @@ class IntentHandler: INExtension {
|
|||
return FollowersCountIntentHandler()
|
||||
case is MultiFollowersCountIntent:
|
||||
return MultiFollowersCountIntentHandler()
|
||||
case is HashtagIntent:
|
||||
return HashtagIntentHandler()
|
||||
default:
|
||||
return self
|
||||
}
|
||||
|
|
|
@ -1543,6 +1543,32 @@ public enum L10n {
|
|||
/// FOLLOWERS
|
||||
public static let title = L10n.tr("Localizable", "Widget.FollowersCount.Title", fallback: "FOLLOWERS")
|
||||
}
|
||||
public enum Hashtag {
|
||||
public enum Configuration {
|
||||
/// Shows a recent post with the selected hashtag
|
||||
public static let description = L10n.tr("Localizable", "Widget.Hashtag.Configuration.Description", fallback: "Shows a recent post with the selected hashtag")
|
||||
/// Hashtag
|
||||
public static let displayName = L10n.tr("Localizable", "Widget.Hashtag.Configuration.DisplayName", fallback: "Hashtag")
|
||||
}
|
||||
public enum NotFound {
|
||||
/// @johnMastodon@no-such.account
|
||||
public static let account = L10n.tr("Localizable", "Widget.Hashtag.NotFound.Account", fallback: "@johnMastodon@no-such.account")
|
||||
/// John Mastodon
|
||||
public static let accountName = L10n.tr("Localizable", "Widget.Hashtag.NotFound.AccountName", fallback: "John Mastodon")
|
||||
/// Sorry, we couldn’t find any posts with the hashtag <a>#%@</a>. Please try a <a>#DifferentHashtag</a> or check the widget settings
|
||||
public static func content(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Widget.Hashtag.NotFound.Content", String(describing: p1), fallback: "Sorry, we couldn’t find any posts with the hashtag <a>#%@</a>. Please try a <a>#DifferentHashtag</a> or check the widget settings")
|
||||
}
|
||||
}
|
||||
public enum Placeholder {
|
||||
/// @johnMastodon@no-such.account
|
||||
public static let account = L10n.tr("Localizable", "Widget.Hashtag.Placeholder.Account", fallback: "@johnMastodon@no-such.account")
|
||||
/// John Mastodon
|
||||
public static let accountName = L10n.tr("Localizable", "Widget.Hashtag.Placeholder.AccountName", fallback: "John Mastodon")
|
||||
/// This is how a post with a <a>#hashtag</a> would look. Pick whichever <a>#hashtag</a> you want in the widget settings
|
||||
public static let content = L10n.tr("Localizable", "Widget.Hashtag.Placeholder.Content", fallback: "This is how a post with a <a>#hashtag</a> would look. Pick whichever <a>#hashtag</a> you want in the widget settings")
|
||||
}
|
||||
}
|
||||
public enum LatestFollowers {
|
||||
/// Show latest followers.
|
||||
public static let configurationDescription = L10n.tr("Localizable", "Widget.LatestFollowers.ConfigurationDescription", fallback: "Show latest followers.")
|
||||
|
|
|
@ -543,3 +543,13 @@ uploaded to Mastodon.";
|
|||
"Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers";
|
||||
"Widget.MultipleFollowers.MockUser.AccountName" = "another@follower.social";
|
||||
"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower";
|
||||
"Widget.Hashtag.Configuration.Description" = "Shows a recent post with the selected hashtag";
|
||||
"Widget.Hashtag.Configuration.DisplayName" = "Hashtag";
|
||||
|
||||
"Widget.Hashtag.NotFound.AccountName" = "John Mastodon";
|
||||
"Widget.Hashtag.NotFound.Account" = "@johnMastodon@no-such.account";
|
||||
"Widget.Hashtag.NotFound.Content" = "Sorry, we couldn’t find any posts with the hashtag <a>#%@</a>. Please try a <a>#DifferentHashtag</a> or check the widget settings";
|
||||
|
||||
"Widget.Hashtag.Placeholder.AccountName" = "John Mastodon";
|
||||
"Widget.Hashtag.Placeholder.Account" = "@johnMastodon@no-such.account";
|
||||
"Widget.Hashtag.Placeholder.Content" = "This is how a post with a <a>#hashtag</a> would look. Pick whichever <a>#hashtag</a> you want in the widget settings";
|
||||
|
|
|
@ -543,3 +543,13 @@ uploaded to Mastodon.";
|
|||
"Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers";
|
||||
"Widget.MultipleFollowers.MockUser.AccountName" = "another@follower.social";
|
||||
"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower";
|
||||
"Widget.Hashtag.Configuration.Description" = "Shows a recent post with the selected hashtag";
|
||||
"Widget.Hashtag.Configuration.DisplayName" = "Hashtag";
|
||||
|
||||
"Widget.Hashtag.NotFound.AccountName" = "John Mastodon";
|
||||
"Widget.Hashtag.NotFound.Account" = "@johnMastodon@no-such.account";
|
||||
"Widget.Hashtag.NotFound.Content" = "Sorry, we couldn’t find any posts with the hashtag <a>#%@</a>. Please try a <a>#DifferentHashtag</a> or check the widget settings";
|
||||
|
||||
"Widget.Hashtag.Placeholder.AccountName" = "John Mastodon";
|
||||
"Widget.Hashtag.Placeholder.Account" = "@johnMastodon@no-such.account";
|
||||
"Widget.Hashtag.Placeholder.Content" = "This is how a post with a <a>#hashtag</a> would look. Pick whichever <a>#hashtag</a> you want in the widget settings";
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIColor {
|
||||
public var hexValue: String {
|
||||
let components = cgColor.components
|
||||
|
||||
let red: CGFloat = components?[0] ?? 0.0
|
||||
let green: CGFloat = components?[1] ?? 0.0
|
||||
let blue: CGFloat = components?[2] ?? 0.0
|
||||
|
||||
return String(
|
||||
format: "#%02lX%02lX%02lX",
|
||||
lroundf(Float(red * 255)),
|
||||
lroundf(Float(green * 255)),
|
||||
lroundf(Float(blue * 255))
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.984",
|
||||
"green" : "0.173",
|
||||
"red" : "0.333"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.980",
|
||||
"green" : "0.541",
|
||||
"red" : "0.522"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.000",
|
||||
"green" : "0.000",
|
||||
"red" : "0.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -9,11 +9,11 @@
|
|||
<key>INIntentDefinitionNamespace</key>
|
||||
<string>88xZPY</string>
|
||||
<key>INIntentDefinitionSystemVersion</key>
|
||||
<string>22D49</string>
|
||||
<string>22D68</string>
|
||||
<key>INIntentDefinitionToolsBuildVersion</key>
|
||||
<string>14C18</string>
|
||||
<string>14E222b</string>
|
||||
<key>INIntentDefinitionToolsVersion</key>
|
||||
<string>14.2</string>
|
||||
<string>14.3</string>
|
||||
<key>INIntents</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
@ -348,6 +348,8 @@
|
|||
<true/>
|
||||
<key>INIntentIneligibleForSuggestions</key>
|
||||
<true/>
|
||||
<key>INIntentLastParameterTag</key>
|
||||
<integer>1</integer>
|
||||
<key>INIntentName</key>
|
||||
<string>LatestFollowers</string>
|
||||
<key>INIntentResponse</key>
|
||||
|
@ -375,6 +377,137 @@
|
|||
<key>INIntentVerb</key>
|
||||
<string>View</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>INIntentCategory</key>
|
||||
<string>information</string>
|
||||
<key>INIntentDescription</key>
|
||||
<string>Hashtag</string>
|
||||
<key>INIntentDescriptionID</key>
|
||||
<string>A1rwKl</string>
|
||||
<key>INIntentEligibleForWidgets</key>
|
||||
<true/>
|
||||
<key>INIntentIneligibleForSuggestions</key>
|
||||
<true/>
|
||||
<key>INIntentLastParameterTag</key>
|
||||
<integer>7</integer>
|
||||
<key>INIntentName</key>
|
||||
<string>Hashtag</string>
|
||||
<key>INIntentParameters</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>INIntentParameterConfigurable</key>
|
||||
<true/>
|
||||
<key>INIntentParameterDisplayName</key>
|
||||
<string>Hashtag</string>
|
||||
<key>INIntentParameterDisplayNameID</key>
|
||||
<string>GTUbZg</string>
|
||||
<key>INIntentParameterDisplayPriority</key>
|
||||
<integer>1</integer>
|
||||
<key>INIntentParameterMetadata</key>
|
||||
<dict>
|
||||
<key>INIntentParameterMetadataCapitalization</key>
|
||||
<string>Sentences</string>
|
||||
<key>INIntentParameterMetadataDefaultValueID</key>
|
||||
<string>YdkgW1</string>
|
||||
</dict>
|
||||
<key>INIntentParameterName</key>
|
||||
<string>hashtag</string>
|
||||
<key>INIntentParameterPromptDialogs</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>INIntentParameterPromptDialogCustom</key>
|
||||
<true/>
|
||||
<key>INIntentParameterPromptDialogFormatString</key>
|
||||
<string>Hashtag</string>
|
||||
<key>INIntentParameterPromptDialogFormatStringID</key>
|
||||
<string>zbXop9</string>
|
||||
<key>INIntentParameterPromptDialogType</key>
|
||||
<string>Configuration</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>INIntentParameterPromptDialogCustom</key>
|
||||
<true/>
|
||||
<key>INIntentParameterPromptDialogType</key>
|
||||
<string>Primary</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>INIntentParameterSupportsDynamicEnumeration</key>
|
||||
<true/>
|
||||
<key>INIntentParameterSupportsSearch</key>
|
||||
<true/>
|
||||
<key>INIntentParameterTag</key>
|
||||
<integer>5</integer>
|
||||
<key>INIntentParameterType</key>
|
||||
<string>String</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>INIntentParameterConfigurable</key>
|
||||
<true/>
|
||||
<key>INIntentParameterDisplayName</key>
|
||||
<string>Ignore content warnings</string>
|
||||
<key>INIntentParameterDisplayNameID</key>
|
||||
<string>xcBHPA</string>
|
||||
<key>INIntentParameterDisplayPriority</key>
|
||||
<integer>2</integer>
|
||||
<key>INIntentParameterMetadata</key>
|
||||
<dict>
|
||||
<key>INIntentParameterMetadataFalseDisplayName</key>
|
||||
<string>false</string>
|
||||
<key>INIntentParameterMetadataFalseDisplayNameID</key>
|
||||
<string>wftYbm</string>
|
||||
<key>INIntentParameterMetadataTrueDisplayName</key>
|
||||
<string>true</string>
|
||||
<key>INIntentParameterMetadataTrueDisplayNameID</key>
|
||||
<string>QkKsLf</string>
|
||||
</dict>
|
||||
<key>INIntentParameterName</key>
|
||||
<string>ignoreContentWarnings</string>
|
||||
<key>INIntentParameterPromptDialogs</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>INIntentParameterPromptDialogCustom</key>
|
||||
<true/>
|
||||
<key>INIntentParameterPromptDialogType</key>
|
||||
<string>Configuration</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>INIntentParameterPromptDialogCustom</key>
|
||||
<true/>
|
||||
<key>INIntentParameterPromptDialogType</key>
|
||||
<string>Primary</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>INIntentParameterTag</key>
|
||||
<integer>7</integer>
|
||||
<key>INIntentParameterType</key>
|
||||
<string>Boolean</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>INIntentResponse</key>
|
||||
<dict>
|
||||
<key>INIntentResponseCodes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>INIntentResponseCodeName</key>
|
||||
<string>success</string>
|
||||
<key>INIntentResponseCodeSuccess</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>INIntentResponseCodeName</key>
|
||||
<string>failure</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>INIntentTitle</key>
|
||||
<string>Hashtag</string>
|
||||
<key>INIntentTitleID</key>
|
||||
<string>OcUp1W</string>
|
||||
<key>INIntentType</key>
|
||||
<string>Custom</string>
|
||||
<key>INIntentVerb</key>
|
||||
<string>View</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>INTypes</key>
|
||||
<array/>
|
||||
|
|
|
@ -127,7 +127,7 @@ struct FollowersCountWidgetView: View {
|
|||
.padding(.top, 16)
|
||||
}
|
||||
|
||||
private func viewForAccessoryRectangular(_ account :FollowersEntryAccountable) -> some View {
|
||||
private func viewForAccessoryRectangular(_ account: FollowersEntryAccountable) -> some View {
|
||||
HStack(spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(alignment: .center) {
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
import MastodonSDK
|
||||
import MastodonLocalization
|
||||
|
||||
struct HashtagWidgetProvider: IntentTimelineProvider {
|
||||
func placeholder(in context: Context) -> HashtagWidgetTimelineEntry {
|
||||
.placeholder
|
||||
}
|
||||
|
||||
func getSnapshot(for configuration: HashtagIntent, in context: Context, completion: @escaping (HashtagWidgetTimelineEntry) -> Void) {
|
||||
loadMostRecentHashtag(for: configuration, in: context, completion: completion)
|
||||
}
|
||||
|
||||
func getTimeline(for configuration: HashtagIntent, in context: Context, completion: @escaping (Timeline<HashtagWidgetTimelineEntry>) -> Void) {
|
||||
loadMostRecentHashtag(for: configuration, in: context) { entry in
|
||||
completion(Timeline(entries: [entry], policy: .after(.now.addingTimeInterval(60 * 15))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension HashtagWidgetProvider {
|
||||
func loadMostRecentHashtag(for configuration: HashtagIntent, in context: Context, completion: @escaping (HashtagWidgetTimelineEntry) -> Void ) {
|
||||
|
||||
guard
|
||||
let authBox = WidgetExtension.appContext
|
||||
.authenticationService
|
||||
.mastodonAuthenticationBoxes
|
||||
.first
|
||||
else {
|
||||
if context.isPreview {
|
||||
return completion(.placeholder)
|
||||
} else {
|
||||
return completion(.unconfigured)
|
||||
}
|
||||
}
|
||||
|
||||
let desiredHashtag: String
|
||||
|
||||
if let hashtag = configuration.hashtag {
|
||||
desiredHashtag = hashtag
|
||||
} else {
|
||||
return completion(.notFound("hashtag"))
|
||||
}
|
||||
|
||||
Task {
|
||||
|
||||
do {
|
||||
let mostRecentStatuses = try await WidgetExtension.appContext
|
||||
.apiService
|
||||
.hashtagTimeline(domain: authBox.domain, limit: 40, hashtag: desiredHashtag, authenticationBox: authBox)
|
||||
.value
|
||||
|
||||
let filteredStatuses: [Mastodon.Entity.Status]
|
||||
if configuration.ignoreContentWarnings?.boolValue == true {
|
||||
filteredStatuses = mostRecentStatuses
|
||||
} else {
|
||||
filteredStatuses = mostRecentStatuses.filter { $0.sensitive == false }
|
||||
}
|
||||
|
||||
if let mostRecentStatus = filteredStatuses.first {
|
||||
|
||||
let hashtagEntry = HashtagEntry(
|
||||
accountName: mostRecentStatus.account.displayNameWithFallback,
|
||||
account: mostRecentStatus.account.acct,
|
||||
content: mostRecentStatus.content ?? "-",
|
||||
reblogCount: mostRecentStatus.reblogsCount,
|
||||
favoriteCount: mostRecentStatus.favouritesCount,
|
||||
hashtag: "#\(desiredHashtag)",
|
||||
timestamp: mostRecentStatus.createdAt
|
||||
)
|
||||
|
||||
let hashtagTimelineEntry = HashtagWidgetTimelineEntry(
|
||||
date: mostRecentStatus.createdAt,
|
||||
hashtag: hashtagEntry
|
||||
)
|
||||
|
||||
completion(hashtagTimelineEntry)
|
||||
} else {
|
||||
let noStatusFound = HashtagWidgetTimelineEntry.notFound(desiredHashtag)
|
||||
|
||||
completion(noStatusFound)
|
||||
}
|
||||
} catch {
|
||||
completion(.notFound(desiredHashtag))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct HashtagWidgetTimelineEntry: TimelineEntry {
|
||||
var date: Date
|
||||
var hashtag: HashtagEntry
|
||||
|
||||
static var placeholder: Self {
|
||||
HashtagWidgetTimelineEntry(
|
||||
date: .now,
|
||||
hashtag: HashtagEntry(
|
||||
accountName: L10n.Widget.Hashtag.Placeholder.accountName,
|
||||
account: L10n.Widget.Hashtag.Placeholder.account,
|
||||
content: L10n.Widget.Hashtag.Placeholder.content,
|
||||
reblogCount: 13,
|
||||
favoriteCount: 12,
|
||||
hashtag: "#hashtag",
|
||||
timestamp: .now.addingTimeInterval(-3600 * 12)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
static func notFound(_ hashtag: String? = nil) -> Self {
|
||||
HashtagWidgetTimelineEntry(
|
||||
date: .now,
|
||||
hashtag: HashtagEntry(
|
||||
accountName: L10n.Widget.Hashtag.NotFound.accountName,
|
||||
account: L10n.Widget.Hashtag.NotFound.account,
|
||||
content: L10n.Widget.Hashtag.NotFound.content(hashtag ?? "hashtag"),
|
||||
reblogCount: 0,
|
||||
favoriteCount: 0,
|
||||
hashtag: hashtag ?? "",
|
||||
timestamp: .now
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
static var unconfigured: Self {
|
||||
HashtagWidgetTimelineEntry(
|
||||
date: .now,
|
||||
hashtag: HashtagEntry(
|
||||
accountName: "Unconfigured",
|
||||
account: "@unconfigured@mastodon.social",
|
||||
content: "Caturday is the best day of the week #CatsOfMastodon",
|
||||
reblogCount: 14,
|
||||
favoriteCount: 13,
|
||||
hashtag: "#CatsOfMastodon",
|
||||
timestamp: .now.addingTimeInterval(-3600 * 18)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct HashtagWidget: Widget {
|
||||
|
||||
private var availableFamilies: [WidgetFamily] {
|
||||
if #available(iOS 16, *) {
|
||||
return [.systemMedium, .systemLarge, .accessoryRectangular]
|
||||
} else {
|
||||
return [.systemMedium, .systemLarge]
|
||||
}
|
||||
}
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
IntentConfiguration(kind: "Hashtag", intent: HashtagIntent.self, provider: HashtagWidgetProvider()) { entry in
|
||||
HashtagWidgetView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName(L10n.Widget.Hashtag.Configuration.displayName)
|
||||
.description(L10n.Widget.Hashtag.Configuration.description)
|
||||
.supportedFamilies(availableFamilies)
|
||||
}
|
||||
}
|
||||
|
||||
struct HashtagEntry {
|
||||
var accountName: String
|
||||
var account: String
|
||||
var content: String
|
||||
var reblogCount: Int
|
||||
var favoriteCount: Int
|
||||
var hashtag: String
|
||||
var timestamp: Date
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
import MastodonLocalization
|
||||
|
||||
struct HashtagWidgetView: View {
|
||||
|
||||
var entry: HashtagWidgetProvider.Entry
|
||||
|
||||
@Environment(\.widgetFamily) var family
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var body: some View {
|
||||
switch family {
|
||||
case .systemMedium, .systemLarge:
|
||||
viewForMediumWidget(colorScheme: colorScheme)
|
||||
case .accessoryRectangular:
|
||||
viewForRectangularAccessory()
|
||||
default:
|
||||
Text(L10n.Widget.Common.unsupportedWidgetFamily)
|
||||
}
|
||||
}
|
||||
|
||||
private func viewForMediumWidget(colorScheme: ColorScheme) -> some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack {
|
||||
Text(entry.hashtag.accountName)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
Text(entry.hashtag.account)
|
||||
.lineLimit(1)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text(entry.hashtag.timestamp.localizedShortTimeAgo(since: .now))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text(statusHTML: entry.hashtag.content, colorScheme: colorScheme)
|
||||
|
||||
Spacer()
|
||||
HStack(alignment: .center, spacing: 16) {
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: "arrow.2.squarepath")
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(entry.hashtag.reblogCount)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: "star")
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(entry.hashtag.favoriteCount)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Text(entry.hashtag.hashtag)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(EdgeInsets(top: 12, leading: 29, bottom: 12, trailing: 29))
|
||||
}
|
||||
|
||||
private func viewForRectangularAccessory() -> some View {
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
HStack(alignment: .center, spacing: 3) {
|
||||
Image("BrandIcon")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.frame(width: 10, height: 10)
|
||||
.foregroundColor(.secondary)
|
||||
Text("|")
|
||||
.font(.system(size: UIFontMetrics.default.scaledValue(for: 12)))
|
||||
.foregroundColor(.secondary)
|
||||
Text(entry.hashtag.hashtag)
|
||||
.font(.system(size: UIFontMetrics.default.scaledValue(for: 13)))
|
||||
.fontWeight(.heavy)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Text(statusHTML: entry.hashtag.content, fontSize: 11, fontWeight: 510)
|
||||
.lineLimit(3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Inspired by: https://swiftuirecipes.com/blog/swiftui-text-with-html-via-nsattributedstring
|
||||
extension Text {
|
||||
init(statusHTML htmlString: String, fontSize: Int = 16, fontWeight: Int = 400, colorScheme: ColorScheme = .light) {
|
||||
|
||||
let textColor = (UIColor(named: "Colors/TextColor") ?? UIColor.gray).hexValue
|
||||
let accentColor = (UIColor(named: "Colors/Blurple") ?? UIColor.purple).hexValue
|
||||
|
||||
let fullHTML = """
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system;
|
||||
font-size: \(fontSize)px;
|
||||
font-weight: \(fontWeight);
|
||||
line-height: 133%;
|
||||
color: \(textColor);
|
||||
}
|
||||
|
||||
a {
|
||||
color: \(accentColor);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
\(htmlString)
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
let attributedString: NSAttributedString
|
||||
if let data = fullHTML.data(using: .unicode),
|
||||
let attrString = try? NSAttributedString(data: data,
|
||||
options: [.documentType: NSAttributedString.DocumentType.html],
|
||||
documentAttributes: nil) {
|
||||
attributedString = attrString
|
||||
} else {
|
||||
attributedString = NSAttributedString(string: htmlString)
|
||||
}
|
||||
|
||||
self.init(AttributedString(attributedString)) // uses the NSAttributedString initializer
|
||||
}
|
||||
}
|
|
@ -9,5 +9,6 @@ struct WidgetExtensionBundle: WidgetBundle {
|
|||
FollowersCountWidget()
|
||||
MultiFollowersCountWidget()
|
||||
LatestFollowersWidget()
|
||||
HashtagWidget()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue