forked from zelo72/mastodon-ios
Merge branch 'release/0.9.3'
This commit is contained in:
commit
f7a0df91a8
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20F71" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20G71" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Application" representedClassName=".Application" syncable="YES">
|
||||
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
|
@ -195,6 +195,7 @@
|
|||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="preferredStaticAvatar" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="preferredStaticEmoji" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="preferredTrueBlackDarkMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="preferredUsingDefaultBrowser" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
|
@ -290,7 +291,7 @@
|
|||
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="PrivateNote" positionX="0" positionY="0" width="128" height="89"/>
|
||||
<element name="SearchHistory" positionX="0" positionY="0" width="128" height="149"/>
|
||||
<element name="Setting" positionX="72" positionY="162" width="128" height="164"/>
|
||||
<element name="Setting" positionX="72" positionY="162" width="128" height="179"/>
|
||||
<element name="Status" positionX="0" positionY="0" width="128" height="614"/>
|
||||
<element name="Subscription" positionX="81" positionY="171" width="128" height="179"/>
|
||||
<element name="SubscriptionAlerts" positionX="72" positionY="162" width="128" height="14"/>
|
||||
|
|
|
@ -16,6 +16,7 @@ public final class Setting: NSManagedObject {
|
|||
@NSManaged public var appearanceRaw: String
|
||||
@NSManaged public var preferredTrueBlackDarkMode: Bool
|
||||
@NSManaged public var preferredStaticAvatar: Bool
|
||||
@NSManaged public var preferredStaticEmoji: Bool
|
||||
@NSManaged public var preferredUsingDefaultBrowser: Bool
|
||||
|
||||
@NSManaged public private(set) var createdAt: Date
|
||||
|
@ -64,6 +65,12 @@ extension Setting {
|
|||
didUpdate(at: Date())
|
||||
}
|
||||
|
||||
public func update(preferredStaticEmoji: Bool) {
|
||||
guard preferredStaticEmoji != self.preferredStaticEmoji else { return }
|
||||
self.preferredStaticEmoji = preferredStaticEmoji
|
||||
didUpdate(at: Date())
|
||||
}
|
||||
|
||||
public func update(preferredUsingDefaultBrowser: Bool) {
|
||||
guard preferredUsingDefaultBrowser != self.preferredUsingDefaultBrowser else { return }
|
||||
self.preferredUsingDefaultBrowser = preferredUsingDefaultBrowser
|
||||
|
|
|
@ -500,10 +500,6 @@
|
|||
"light": "Always Light",
|
||||
"dark": "Always Dark"
|
||||
},
|
||||
"appearance_settings": {
|
||||
"true_black_dark_mode": "True black dark mode",
|
||||
"disable_avatar_animation": "Disable animated avatars"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"favorites": "Favorites my post",
|
||||
|
@ -520,6 +516,9 @@
|
|||
},
|
||||
"preference": {
|
||||
"title": "Preferences",
|
||||
"true_black_dark_mode": "True black dark mode",
|
||||
"disable_avatar_animation": "Disable animated avatars",
|
||||
"disable_emoji_animation": "Disable animated emojis",
|
||||
"using_default_browser": "Use default browser to open links"
|
||||
},
|
||||
"boring_zone": {
|
||||
|
|
|
@ -65,7 +65,6 @@
|
|||
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1FD25CD481700561493 /* StatusProvider.swift */; };
|
||||
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; };
|
||||
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */; };
|
||||
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; };
|
||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; };
|
||||
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */; };
|
||||
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8E25C8228A004A627A /* UIButton.swift */; };
|
||||
|
@ -179,12 +178,12 @@
|
|||
DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* CommonOSLog */; };
|
||||
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; };
|
||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
|
||||
DB01E23326A98F0900C3965B /* MastodonMeta in Frameworks */ = {isa = PBXBuildFile; productRef = DB01E23226A98F0900C3965B /* MastodonMeta */; };
|
||||
DB01E23526A98F0900C3965B /* MetaTextKit in Frameworks */ = {isa = PBXBuildFile; productRef = DB01E23426A98F0900C3965B /* MetaTextKit */; };
|
||||
DB023295267F0AB800031745 /* ASMetaEditableTextNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB023294267F0AB800031745 /* ASMetaEditableTextNode.swift */; };
|
||||
DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB029E94266A20430062874E /* MastodonAuthenticationController.swift */; };
|
||||
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; };
|
||||
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; };
|
||||
DB03F7EB268976B5007B274C /* MastodonMeta in Frameworks */ = {isa = PBXBuildFile; productRef = DB03F7EA268976B5007B274C /* MastodonMeta */; };
|
||||
DB03F7ED268976B5007B274C /* MetaTextView in Frameworks */ = {isa = PBXBuildFile; productRef = DB03F7EC268976B5007B274C /* MetaTextView */; };
|
||||
DB03F7F32689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */; };
|
||||
DB03F7F52689B782007B274C /* ComposeTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F42689B782007B274C /* ComposeTableView.swift */; };
|
||||
DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ED026538E3C00BEE9D8 /* Trie.swift */; };
|
||||
|
@ -196,6 +195,7 @@
|
|||
DB0C946F26A7D2A80088FB11 /* AvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0C946E26A7D2A80088FB11 /* AvatarImageView.swift */; };
|
||||
DB0C947226A7D2D70088FB11 /* AvatarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0C947126A7D2D70088FB11 /* AvatarButton.swift */; };
|
||||
DB0C947726A7FE840088FB11 /* NotificationAvatarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0C947626A7FE840088FB11 /* NotificationAvatarButton.swift */; };
|
||||
DB0E91EA26A9675100BD2ACC /* MetaLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0E91E926A9675100BD2ACC /* MetaLabel.swift */; };
|
||||
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */; };
|
||||
DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; };
|
||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */; };
|
||||
|
@ -227,8 +227,6 @@
|
|||
DB3667A8268AE2900027D07F /* ComposeStatusPollItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */; };
|
||||
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; };
|
||||
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; };
|
||||
DB41ED8226A54D8A00F58330 /* MastodonMeta in Frameworks */ = {isa = PBXBuildFile; productRef = DB41ED8126A54D8A00F58330 /* MastodonMeta */; };
|
||||
DB41ED8426A54D8A00F58330 /* MetaTextView in Frameworks */ = {isa = PBXBuildFile; productRef = DB41ED8326A54D8A00F58330 /* MetaTextView */; };
|
||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
|
||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; };
|
||||
DB427DDD25BAA00100D1B89D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDB25BAA00100D1B89D /* Main.storyboard */; };
|
||||
|
@ -310,7 +308,6 @@
|
|||
DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
DB6804922637CD8700430867 /* AppName.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804912637CD8700430867 /* AppName.swift */; };
|
||||
DB6804A52637CDCC00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; };
|
||||
DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; };
|
||||
DB6804D12637CE4700430867 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804D02637CE4700430867 /* UserDefaults.swift */; };
|
||||
DB6804FD2637CFEC00430867 /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804FC2637CFEC00430867 /* AppSecret.swift */; };
|
||||
DB6805102637D0F800430867 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = DB68050F2637D0F800430867 /* KeychainAccess */; };
|
||||
|
@ -443,7 +440,6 @@
|
|||
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; };
|
||||
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; };
|
||||
DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */; };
|
||||
DBAEDE61267B342D00D25FF5 /* StatusContentCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */; };
|
||||
DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; };
|
||||
DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; };
|
||||
DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; };
|
||||
|
@ -480,11 +476,7 @@
|
|||
DBBC24D126A5484F00398BB9 /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DBBC24D026A5484F00398BB9 /* UITextView+Placeholder */; };
|
||||
DBBC24D226A5488600398BB9 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24B426A540AE00398BB9 /* AvatarConfigurableView.swift */; };
|
||||
DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */; };
|
||||
DBBC24DD26A54BCB00398BB9 /* MastodonStatusContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D726A54BCB00398BB9 /* MastodonStatusContent.swift */; };
|
||||
DBBC24DE26A54BCB00398BB9 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */; };
|
||||
DBBC24DF26A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D926A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift */; };
|
||||
DBBC24E026A54BCB00398BB9 /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24DA26A54BCB00398BB9 /* MastodonField.swift */; };
|
||||
DBBC24E126A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24DB26A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift */; };
|
||||
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
|
||||
DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */; };
|
||||
DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */; };
|
||||
|
@ -896,6 +888,7 @@
|
|||
DB0C946E26A7D2A80088FB11 /* AvatarImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarImageView.swift; sourceTree = "<group>"; };
|
||||
DB0C947126A7D2D70088FB11 /* AvatarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarButton.swift; sourceTree = "<group>"; };
|
||||
DB0C947626A7FE840088FB11 /* NotificationAvatarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAvatarButton.swift; sourceTree = "<group>"; };
|
||||
DB0E91E926A9675100BD2ACC /* MetaLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaLabel.swift; sourceTree = "<group>"; };
|
||||
DB0F814D264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
DB0F814E264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||
|
@ -930,6 +923,7 @@
|
|||
DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollItem.swift; sourceTree = "<group>"; };
|
||||
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
|
||||
DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
DB3F693926AA97BD00C883AB /* MetaTextKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = MetaTextKit; path = ../MetaTextKit; sourceTree = "<group>"; };
|
||||
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DB427DD525BAA00100D1B89D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
DB427DD725BAA00100D1B89D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
|
@ -1143,7 +1137,6 @@
|
|||
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Mute.swift"; sourceTree = "<group>"; };
|
||||
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; };
|
||||
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurhashImageCacheService.swift; sourceTree = "<group>"; };
|
||||
DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentCacheService.swift; sourceTree = "<group>"; };
|
||||
DBAFB7342645463500371D5F /* Emojis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emojis.swift; sourceTree = "<group>"; };
|
||||
DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLAnimatedImageView.swift; sourceTree = "<group>"; };
|
||||
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -1168,11 +1161,7 @@
|
|||
DBBC24C326A544B900398BB9 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
|
||||
DBBC24CE26A547AE00398BB9 /* ThemeService+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThemeService+Appearance.swift"; sourceTree = "<group>"; };
|
||||
DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonRegex.swift; sourceTree = "<group>"; };
|
||||
DBBC24D726A54BCB00398BB9 /* MastodonStatusContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonStatusContent.swift; sourceTree = "<group>"; };
|
||||
DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = "<group>"; };
|
||||
DBBC24D926A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MastodonStatusContent+ParseResult.swift"; sourceTree = "<group>"; };
|
||||
DBBC24DA26A54BCB00398BB9 /* MastodonField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonField.swift; sourceTree = "<group>"; };
|
||||
DBBC24DB26A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MastodonStatusContent+Appearance.swift"; sourceTree = "<group>"; };
|
||||
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
|
||||
DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewModel.swift; sourceTree = "<group>"; };
|
||||
DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteTableViewCell.swift; sourceTree = "<group>"; };
|
||||
|
@ -1257,10 +1246,8 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
|
||||
DB03F7ED268976B5007B274C /* MetaTextView in Frameworks */,
|
||||
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
|
||||
DBC6462B26A1738900B0E31B /* MastodonUI in Frameworks */,
|
||||
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
|
||||
DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */,
|
||||
2D939AC825EE14620076FA61 /* CropViewController in Frameworks */,
|
||||
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */,
|
||||
|
@ -1269,14 +1256,14 @@
|
|||
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */,
|
||||
DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */,
|
||||
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
|
||||
DB01E23326A98F0900C3965B /* MastodonMeta in Frameworks */,
|
||||
DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */,
|
||||
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */,
|
||||
DBAC649E267DFE43007FE9FD /* DiffableDataSources in Frameworks */,
|
||||
2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */,
|
||||
87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */,
|
||||
DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */,
|
||||
DB03F7EB268976B5007B274C /* MastodonMeta in Frameworks */,
|
||||
DBF7A0FC26830C33004176A2 /* FPSIndicator in Frameworks */,
|
||||
DB01E23526A98F0900C3965B /* MetaTextKit in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -1329,8 +1316,6 @@
|
|||
DBBC24B826A5421800398BB9 /* CommonOSLog in Frameworks */,
|
||||
DBC6462526A1720B00B0E31B /* MastodonUI in Frameworks */,
|
||||
DBC6463726A195DB00B0E31B /* CoreDataStack.framework in Frameworks */,
|
||||
DB41ED8426A54D8A00F58330 /* MetaTextView in Frameworks */,
|
||||
DB41ED8226A54D8A00F58330 /* MastodonMeta in Frameworks */,
|
||||
DBBC24D126A5484F00398BB9 /* UITextView+Placeholder in Frameworks */,
|
||||
DBBC24AA26A5301B00398BB9 /* MastodonSDK in Frameworks */,
|
||||
DB0C946526A6FD4D0088FB11 /* AlamofireImage in Frameworks */,
|
||||
|
@ -1602,7 +1587,6 @@
|
|||
DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */,
|
||||
DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */,
|
||||
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */,
|
||||
DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */,
|
||||
DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */,
|
||||
);
|
||||
path = Service;
|
||||
|
@ -1976,6 +1960,7 @@
|
|||
children = (
|
||||
DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */,
|
||||
DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */,
|
||||
DB3F693926AA97BD00C883AB /* MetaTextKit */,
|
||||
DB3D0FED25BAA42200EAA174 /* MastodonSDK */,
|
||||
DB427DD425BAA00100D1B89D /* Mastodon */,
|
||||
DB427DEB25BAA00100D1B89D /* MastodonTests */,
|
||||
|
@ -2502,6 +2487,7 @@
|
|||
DB6C8C0525F0921200AAA452 /* MastodonSDK */,
|
||||
DB44384E25E8C1FA008912A2 /* CALayer.swift */,
|
||||
2DF123A625C3B0210020F248 /* ActiveLabel.swift */,
|
||||
DB0E91E926A9675100BD2ACC /* MetaLabel.swift */,
|
||||
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */,
|
||||
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */,
|
||||
DB68A06225E905E000CFDF14 /* UIApplication.swift */,
|
||||
|
@ -2772,11 +2758,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */,
|
||||
DBBC24D726A54BCB00398BB9 /* MastodonStatusContent.swift */,
|
||||
DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */,
|
||||
DBBC24D926A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift */,
|
||||
DBBC24DA26A54BCB00398BB9 /* MastodonField.swift */,
|
||||
DBBC24DB26A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift */,
|
||||
DBFEF07626A691FB006D7ED1 /* MastodonAuthenticationBox.swift */,
|
||||
);
|
||||
path = Helper;
|
||||
|
@ -2989,7 +2971,6 @@
|
|||
DB3D0FF225BAA61700EAA174 /* AlamofireImage */,
|
||||
5D526FE125BE9AC400460CB9 /* MastodonSDK */,
|
||||
2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */,
|
||||
2D42FF6025C8177C004A627A /* ActiveLabel */,
|
||||
DB0140BC25C40D7500F9F3CF /* CommonOSLog */,
|
||||
2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */,
|
||||
2D939AC725EE14620076FA61 /* CropViewController */,
|
||||
|
@ -2999,9 +2980,9 @@
|
|||
DBAC649D267DFE43007FE9FD /* DiffableDataSources */,
|
||||
DBAC64A0267E6D02007FE9FD /* Fuzi */,
|
||||
DBF7A0FB26830C33004176A2 /* FPSIndicator */,
|
||||
DB03F7EA268976B5007B274C /* MastodonMeta */,
|
||||
DB03F7EC268976B5007B274C /* MetaTextView */,
|
||||
DBC6462A26A1738900B0E31B /* MastodonUI */,
|
||||
DB01E23226A98F0900C3965B /* MastodonMeta */,
|
||||
DB01E23426A98F0900C3965B /* MetaTextKit */,
|
||||
);
|
||||
productName = Mastodon;
|
||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||
|
@ -3127,8 +3108,6 @@
|
|||
DBBC24A926A5301B00398BB9 /* MastodonSDK */,
|
||||
DBBC24B726A5421800398BB9 /* CommonOSLog */,
|
||||
DBBC24D026A5484F00398BB9 /* UITextView+Placeholder */,
|
||||
DB41ED8126A54D8A00F58330 /* MastodonMeta */,
|
||||
DB41ED8326A54D8A00F58330 /* MetaTextView */,
|
||||
DB0C946426A6FD4D0088FB11 /* AlamofireImage */,
|
||||
);
|
||||
productName = ShareActionExtension;
|
||||
|
@ -3166,7 +3145,7 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1250;
|
||||
LastUpgradeCheck = 1240;
|
||||
LastUpgradeCheck = 1250;
|
||||
TargetAttributes = {
|
||||
DB427DD125BAA00100D1B89D = {
|
||||
CreatedOnToolsVersion = 12.4;
|
||||
|
@ -3213,20 +3192,18 @@
|
|||
packageReferences = (
|
||||
DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */,
|
||||
2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */,
|
||||
2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */,
|
||||
DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */,
|
||||
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */,
|
||||
2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */,
|
||||
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */,
|
||||
DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */,
|
||||
DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */,
|
||||
DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
|
||||
DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */,
|
||||
DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */,
|
||||
DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */,
|
||||
DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */,
|
||||
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */,
|
||||
DB03F7E9268976B5007B274C /* XCRemoteSwiftPackageReference "MetaTextView" */,
|
||||
DB01E23126A98F0900C3965B /* XCRemoteSwiftPackageReference "MetaTextKit" */,
|
||||
);
|
||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -3555,7 +3532,6 @@
|
|||
DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */,
|
||||
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */,
|
||||
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
|
||||
DBAEDE61267B342D00D25FF5 /* StatusContentCacheService.swift in Sources */,
|
||||
DB0C946F26A7D2A80088FB11 /* AvatarImageView.swift in Sources */,
|
||||
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
||||
|
@ -3715,7 +3691,6 @@
|
|||
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
|
||||
5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */,
|
||||
5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */,
|
||||
DBBC24DF26A54BCB00398BB9 /* MastodonStatusContent+ParseResult.swift in Sources */,
|
||||
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */,
|
||||
DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */,
|
||||
DBAC6488267D388B007FE9FD /* ASTableNode.swift in Sources */,
|
||||
|
@ -3729,7 +3704,6 @@
|
|||
DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */,
|
||||
2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */,
|
||||
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
|
||||
DBBC24E126A54BCB00398BB9 /* MastodonStatusContent+Appearance.swift in Sources */,
|
||||
DBBF1DCB2652539E00E5B703 /* AutoCompleteItem.swift in Sources */,
|
||||
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
|
||||
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */,
|
||||
|
@ -3752,6 +3726,7 @@
|
|||
DB4F096C269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift in Sources */,
|
||||
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
||||
DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */,
|
||||
DB0E91EA26A9675100BD2ACC /* MetaLabel.swift in Sources */,
|
||||
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
||||
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
|
||||
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
||||
|
@ -3788,7 +3763,6 @@
|
|||
DBA465932696B495002B41DB /* APIService+WebFinger.swift in Sources */,
|
||||
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
||||
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */,
|
||||
DBBC24DD26A54BCB00398BB9 /* MastodonStatusContent.swift in Sources */,
|
||||
2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */,
|
||||
DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */,
|
||||
DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */,
|
||||
|
@ -3840,7 +3814,6 @@
|
|||
DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */,
|
||||
DB0C947226A7D2D70088FB11 /* AvatarButton.swift in Sources */,
|
||||
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
||||
DBBC24E026A54BCB00398BB9 /* MastodonField.swift in Sources */,
|
||||
DBFEF07B26A6BCE8006D7ED1 /* APIService+Status+Publish.swift in Sources */,
|
||||
DBCBCC032680AF6E000F5B51 /* AsyncHomeTimelineViewController+DebugAction.swift in Sources */,
|
||||
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */,
|
||||
|
@ -4308,7 +4281,7 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 43;
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -4316,7 +4289,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.9.2;
|
||||
MARKETING_VERSION = 0.9.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -4335,7 +4308,7 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 43;
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -4343,7 +4316,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.9.2;
|
||||
MARKETING_VERSION = 0.9.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -4600,7 +4573,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 43;
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = ShareActionExtension/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -4608,7 +4581,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.9.2;
|
||||
MARKETING_VERSION = 0.9.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -4624,7 +4597,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 43;
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = ShareActionExtension/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -4632,7 +4605,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.9.2;
|
||||
MARKETING_VERSION = 0.9.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -4648,7 +4621,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 43;
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = ShareActionExtension/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -4656,7 +4629,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.9.2;
|
||||
MARKETING_VERSION = 0.9.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -4672,7 +4645,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 43;
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = ShareActionExtension/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -4680,7 +4653,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.9.2;
|
||||
MARKETING_VERSION = 0.9.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -4748,6 +4721,7 @@
|
|||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ASDK;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = "ASDK - Release";
|
||||
|
@ -4761,7 +4735,7 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 43;
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -4769,7 +4743,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.9.2;
|
||||
MARKETING_VERSION = 0.9.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -4876,7 +4850,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 43;
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -4884,7 +4858,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.9.2;
|
||||
MARKETING_VERSION = 0.9.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -4995,7 +4969,7 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 43;
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -5003,7 +4977,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.9.2;
|
||||
MARKETING_VERSION = 0.9.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -5110,7 +5084,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 43;
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -5118,7 +5092,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.9.2;
|
||||
MARKETING_VERSION = 0.9.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -5164,7 +5138,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 43;
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -5172,7 +5146,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.9.2;
|
||||
MARKETING_VERSION = 0.9.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -5187,7 +5161,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 43;
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -5195,7 +5169,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.9.2;
|
||||
MARKETING_VERSION = 0.9.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -5309,14 +5283,6 @@
|
|||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 5.0.3;
|
||||
};
|
||||
};
|
||||
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/vtourraine/ThirdPartyMailer.git";
|
||||
|
@ -5349,12 +5315,12 @@
|
|||
minimumVersion = 0.1.1;
|
||||
};
|
||||
};
|
||||
DB03F7E9268976B5007B274C /* XCRemoteSwiftPackageReference "MetaTextView" */ = {
|
||||
DB01E23126A98F0900C3965B /* XCRemoteSwiftPackageReference "MetaTextKit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/TwidereProject/MetaTextView.git";
|
||||
repositoryURL = "https://github.com/TwidereProject/MetaTextKit.git";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 1.4.0;
|
||||
version = 2.1.0;
|
||||
};
|
||||
};
|
||||
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */ = {
|
||||
|
@ -5381,14 +5347,6 @@
|
|||
minimumVersion = 4.2.2;
|
||||
};
|
||||
};
|
||||
DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/MainasuK/TwitterTextEditor.git";
|
||||
requirement = {
|
||||
branch = "feature/expose-layout";
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/MainasuK/UITextView-Placeholder";
|
||||
|
@ -5440,11 +5398,6 @@
|
|||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
2D42FF6025C8177C004A627A /* ActiveLabel */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */;
|
||||
productName = ActiveLabel;
|
||||
};
|
||||
2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */;
|
||||
|
@ -5474,15 +5427,15 @@
|
|||
package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */;
|
||||
productName = CommonOSLog;
|
||||
};
|
||||
DB03F7EA268976B5007B274C /* MastodonMeta */ = {
|
||||
DB01E23226A98F0900C3965B /* MastodonMeta */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB03F7E9268976B5007B274C /* XCRemoteSwiftPackageReference "MetaTextView" */;
|
||||
package = DB01E23126A98F0900C3965B /* XCRemoteSwiftPackageReference "MetaTextKit" */;
|
||||
productName = MastodonMeta;
|
||||
};
|
||||
DB03F7EC268976B5007B274C /* MetaTextView */ = {
|
||||
DB01E23426A98F0900C3965B /* MetaTextKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB03F7E9268976B5007B274C /* XCRemoteSwiftPackageReference "MetaTextView" */;
|
||||
productName = MetaTextView;
|
||||
package = DB01E23126A98F0900C3965B /* XCRemoteSwiftPackageReference "MetaTextKit" */;
|
||||
productName = MetaTextKit;
|
||||
};
|
||||
DB0C946426A6FD4D0088FB11 /* AlamofireImage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
|
@ -5494,16 +5447,6 @@
|
|||
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
|
||||
productName = AlamofireImage;
|
||||
};
|
||||
DB41ED8126A54D8A00F58330 /* MastodonMeta */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB03F7E9268976B5007B274C /* XCRemoteSwiftPackageReference "MetaTextView" */;
|
||||
productName = MastodonMeta;
|
||||
};
|
||||
DB41ED8326A54D8A00F58330 /* MetaTextView */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB03F7E9268976B5007B274C /* XCRemoteSwiftPackageReference "MetaTextView" */;
|
||||
productName = MetaTextView;
|
||||
};
|
||||
DB68050F2637D0F800430867 /* KeychainAccess */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */;
|
||||
|
|
|
@ -37,12 +37,12 @@
|
|||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>23</integer>
|
||||
<integer>24</integer>
|
||||
</dict>
|
||||
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>21</integer>
|
||||
<integer>23</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -1,22 +1,13 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "ActiveLabel",
|
||||
"repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "d503eb3bfabc54a70139618ab2ba09ebb8c09672",
|
||||
"version": "5.0.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Alamofire",
|
||||
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "4d19ad82f80cc71ff829b941ded114c56f4f604c",
|
||||
"version": "5.4.2"
|
||||
"revision": "f96b619bcb2383b43d898402283924b80e2c4bae",
|
||||
"version": "5.4.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -100,24 +91,6 @@
|
|||
"version": "4.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Kingfisher",
|
||||
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "44450a8f564d7c0165f736ba2250649ff8d3e556",
|
||||
"version": "6.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "MetaTextView",
|
||||
"repositoryURL": "https://github.com/TwidereProject/MetaTextView.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "4c16bc639652a7e1bff4f75e1eba2fcf40213974",
|
||||
"version": "1.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Nuke",
|
||||
"repositoryURL": "https://github.com/kean/Nuke.git",
|
||||
|
@ -145,6 +118,15 @@
|
|||
"version": "3.6.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SDWebImage",
|
||||
"repositoryURL": "https://github.com/SDWebImage/SDWebImage.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "76dd4b49110b8624317fc128e7fa0d8a252018bc",
|
||||
"version": "5.11.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-nio",
|
||||
"repositoryURL": "https://github.com/apple/swift-nio.git",
|
||||
|
@ -208,15 +190,6 @@
|
|||
"version": "2.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "TwitterTextEditor",
|
||||
"repositoryURL": "https://github.com/MainasuK/TwitterTextEditor.git",
|
||||
"state": {
|
||||
"branch": "feature/expose-layout",
|
||||
"revision": "c208329b23dcb3c8c7192de34776440d625a26a4",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "UITextView+Placeholder",
|
||||
"repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder",
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import Foundation
|
||||
import Combine
|
||||
import CoreData
|
||||
import MastodonMeta
|
||||
|
||||
/// Note: update Equatable when change case
|
||||
enum ComposeStatusItem {
|
||||
|
@ -25,7 +26,7 @@ extension ComposeStatusItem {
|
|||
|
||||
let avatarURL = CurrentValueSubject<URL?, Never>(nil)
|
||||
let displayName = CurrentValueSubject<String?, Never>(nil)
|
||||
let emojiDict = CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>([:])
|
||||
let emojiMeta = CurrentValueSubject<MastodonContent.Emojis, Never>([:])
|
||||
let username = CurrentValueSubject<String?, Never>(nil)
|
||||
let composeContent = CurrentValueSubject<String?, Never>(nil)
|
||||
|
||||
|
@ -35,7 +36,7 @@ extension ComposeStatusItem {
|
|||
static func == (lhs: ComposeStatusAttribute, rhs: ComposeStatusAttribute) -> Bool {
|
||||
return lhs.avatarURL.value == rhs.avatarURL.value &&
|
||||
lhs.displayName.value == rhs.displayName.value &&
|
||||
lhs.emojiDict.value == rhs.emojiDict.value &&
|
||||
lhs.emojiMeta.value == rhs.emojiMeta.value &&
|
||||
lhs.username.value == rhs.username.value &&
|
||||
lhs.composeContent.value == rhs.composeContent.value &&
|
||||
lhs.isContentWarningComposing.value == rhs.isContentWarningComposing.value &&
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import Foundation
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
|
||||
enum ProfileFieldItem {
|
||||
case field(field: FieldValue, attribute: FieldItemAttribute)
|
||||
|
@ -56,7 +57,7 @@ extension ProfileFieldItem {
|
|||
|
||||
extension ProfileFieldItem {
|
||||
class FieldItemAttribute: Equatable, ProfileFieldListSeparatorLineConfigurable {
|
||||
let emojiDict = CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>([:])
|
||||
let emojiMeta = CurrentValueSubject<MastodonContent.Emojis, Never>([:])
|
||||
|
||||
var isEditing = false
|
||||
var isLast = false
|
||||
|
|
|
@ -43,12 +43,14 @@ extension SettingsItem {
|
|||
enum PreferenceType: CaseIterable {
|
||||
case darkMode
|
||||
case disableAvatarAnimation
|
||||
case disableEmojiAnimation
|
||||
case useDefaultBrowser
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .darkMode: return L10n.Scene.Settings.Section.AppearanceSettings.trueBlackDarkMode
|
||||
case .disableAvatarAnimation: return L10n.Scene.Settings.Section.AppearanceSettings.disableAvatarAnimation
|
||||
case .darkMode: return L10n.Scene.Settings.Section.Preference.trueBlackDarkMode
|
||||
case .disableAvatarAnimation: return L10n.Scene.Settings.Section.Preference.disableAvatarAnimation
|
||||
case .disableEmojiAnimation: return L10n.Scene.Settings.Section.Preference.disableEmojiAnimation
|
||||
case .useDefaultBrowser: return L10n.Scene.Settings.Section.Preference.usingDefaultBrowser
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import UIKit
|
|||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MetaTextView
|
||||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
import AlamofireImage
|
||||
|
||||
|
@ -45,12 +45,19 @@ extension ComposeStatusSection {
|
|||
// set display name and username
|
||||
Publishers.CombineLatest3(
|
||||
attribute.displayName,
|
||||
attribute.emojiDict,
|
||||
attribute.username.eraseToAnyPublisher()
|
||||
attribute.emojiMeta,
|
||||
attribute.username
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { displayName, emojiDict, username in
|
||||
cell.statusView.nameLabel.configure(content: displayName ?? " ", emojiDict: emojiDict)
|
||||
.sink { displayName, emojiMeta, username in
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: displayName ?? " ", emojis: emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.statusView.nameLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let metaContent = PlaintextMetaContent(string: " ")
|
||||
cell.statusView.nameLabel.configure(content: metaContent)
|
||||
}
|
||||
cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " "
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
|
|
@ -24,8 +24,14 @@ extension CustomEmojiPickerSection {
|
|||
let placeholder = UIImage.placeholder(size: CustomEmojiPickerItemCollectionViewCell.itemSize, color: .systemFill)
|
||||
.af.imageRounded(withCornerRadius: 4)
|
||||
|
||||
let url = URL(string: attribute.emoji.url)
|
||||
cell.emojiImageView.setImage(url: url, placeholder: placeholder, scaleToSize: CustomEmojiPickerItemCollectionViewCell.itemSize)
|
||||
let isAnimated = !UserDefaults.shared.preferredStaticEmoji
|
||||
let url = URL(string: isAnimated ? attribute.emoji.url : attribute.emoji.staticURL)
|
||||
cell.emojiImageView.sd_setImage(
|
||||
with: url,
|
||||
placeholderImage: placeholder,
|
||||
options: [],
|
||||
context: nil
|
||||
)
|
||||
cell.accessibilityLabel = attribute.emoji.shortcode
|
||||
return cell
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import os
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonMeta
|
||||
|
||||
enum ProfileFieldSection: Equatable, Hashable {
|
||||
case main
|
||||
|
@ -29,32 +30,60 @@ extension ProfileFieldSection {
|
|||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ProfileFieldCollectionViewCell.self), for: indexPath) as! ProfileFieldCollectionViewCell
|
||||
|
||||
// set key
|
||||
cell.fieldView.titleActiveLabel.configure(field: field.name.value, emojiDict: attribute.emojiDict.value)
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: field.name.value, emojis: attribute.emojiMeta.value)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.fieldView.titleMetaLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let content = PlaintextMetaContent(string: field.name.value)
|
||||
cell.fieldView.titleMetaLabel.configure(content: content)
|
||||
}
|
||||
cell.fieldView.titleTextField.text = field.name.value
|
||||
Publishers.CombineLatest(
|
||||
field.name.removeDuplicates(),
|
||||
attribute.emojiDict.removeDuplicates()
|
||||
attribute.emojiMeta.removeDuplicates()
|
||||
)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak cell] name, emojiDict in
|
||||
.sink { [weak cell] name, emojiMeta in
|
||||
guard let cell = cell else { return }
|
||||
cell.fieldView.titleActiveLabel.configure(field: name, emojiDict: emojiDict)
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: name, emojis: emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.fieldView.titleMetaLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let content = PlaintextMetaContent(string: name)
|
||||
cell.fieldView.titleMetaLabel.configure(content: content)
|
||||
}
|
||||
// only bind label. The text field should only set once
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
|
||||
// set value
|
||||
cell.fieldView.valueActiveLabel.configure(field: field.value.value, emojiDict: attribute.emojiDict.value)
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: field.value.value, emojis: attribute.emojiMeta.value)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.fieldView.valueMetaLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let content = PlaintextMetaContent(string: field.value.value)
|
||||
cell.fieldView.valueMetaLabel.configure(content: content)
|
||||
}
|
||||
cell.fieldView.valueTextField.text = field.value.value
|
||||
Publishers.CombineLatest(
|
||||
field.value.removeDuplicates(),
|
||||
attribute.emojiDict.removeDuplicates()
|
||||
attribute.emojiMeta.removeDuplicates()
|
||||
)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak cell] value, emojiDict in
|
||||
.sink { [weak cell] value, emojiMeta in
|
||||
guard let cell = cell else { return }
|
||||
cell.fieldView.valueActiveLabel.configure(field: value, emojiDict: emojiDict)
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: value, emojis: emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.fieldView.valueMetaLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let content = PlaintextMetaContent(string: value)
|
||||
cell.fieldView.valueMetaLabel.configure(content: content)
|
||||
}
|
||||
// only bind label. The text field should only set once
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
@ -76,8 +105,8 @@ extension ProfileFieldSection {
|
|||
// setup editing state
|
||||
cell.fieldView.titleTextField.isHidden = !attribute.isEditing
|
||||
cell.fieldView.valueTextField.isHidden = !attribute.isEditing
|
||||
cell.fieldView.titleActiveLabel.isHidden = attribute.isEditing
|
||||
cell.fieldView.valueActiveLabel.isHidden = attribute.isEditing
|
||||
cell.fieldView.titleMetaLabel.isHidden = attribute.isEditing
|
||||
cell.fieldView.valueMetaLabel.isHidden = attribute.isEditing
|
||||
|
||||
// set control hidden
|
||||
let isHidden = !attribute.isEditing
|
||||
|
|
|
@ -125,6 +125,8 @@ extension SettingsSection {
|
|||
cell.switchButton.isOn = setting.preferredTrueBlackDarkMode
|
||||
case .disableAvatarAnimation:
|
||||
cell.switchButton.isOn = setting.preferredStaticAvatar
|
||||
case .disableEmojiAnimation:
|
||||
cell.switchButton.isOn = setting.preferredStaticEmoji
|
||||
case .useDefaultBrowser:
|
||||
cell.switchButton.isOn = setting.preferredUsingDefaultBrowser
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ import CoreDataStack
|
|||
import Foundation
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
|
||||
enum NotificationSection: Equatable, Hashable {
|
||||
case main
|
||||
|
@ -66,7 +68,14 @@ extension NotificationSection {
|
|||
.store(in: &cell.disposeBag)
|
||||
|
||||
// configure author name, notification description, timestamp
|
||||
cell.nameLabel.configure(content: notification.account.displayNameWithFallback, emojiDict: notification.account.emojiDict)
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: notification.account.displayNameWithFallback, emojis: notification.account.emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.nameLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let metaContent = PlaintextMetaContent(string: notification.account.displayNameWithFallback)
|
||||
cell.nameLabel.configure(content: metaContent)
|
||||
}
|
||||
let createAt = notification.createAt
|
||||
let actionText = notification.notificationType.actionText
|
||||
cell.actionLabel.text = actionText + " · " + createAt.timeAgoSinceNow
|
||||
|
|
|
@ -113,7 +113,7 @@ extension StatusSection {
|
|||
accessibilityViews.append(cell.statusView.headerInfoLabel)
|
||||
}
|
||||
accessibilityViews.append(contentsOf: [
|
||||
cell.statusView.nameLabel,
|
||||
cell.statusView.nameMetaLabel,
|
||||
cell.statusView.dateLabel,
|
||||
cell.statusView.contentMetaText.textView,
|
||||
])
|
||||
|
@ -171,7 +171,7 @@ extension StatusSection {
|
|||
cell.statusView.contentMetaText.textView.isAccessibilityElement = false
|
||||
var accessibilityElements: [Any] = []
|
||||
accessibilityElements.append(cell.statusView.avatarView)
|
||||
accessibilityElements.append(cell.statusView.nameLabel)
|
||||
accessibilityElements.append(cell.statusView.nameMetaLabel)
|
||||
accessibilityElements.append(cell.statusView.dateLabel)
|
||||
// TODO: a11y
|
||||
accessibilityElements.append(cell.statusView.contentMetaText.textView)
|
||||
|
@ -652,7 +652,13 @@ extension StatusSection {
|
|||
return L10n.Common.Controls.Status.userReblogged(name)
|
||||
}()
|
||||
// sync set display name to avoid layout issue
|
||||
cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.author.emojiDict)
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: headerText, emojis: status.author.emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.statusView.headerInfoLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
cell.statusView.headerInfoLabel.reset()
|
||||
}
|
||||
cell.statusView.headerInfoLabel.accessibilityLabel = headerText
|
||||
cell.statusView.headerInfoLabel.isAccessibilityElement = true
|
||||
} else if status.inReplyToID != nil {
|
||||
|
@ -666,7 +672,13 @@ extension StatusSection {
|
|||
let name = author.displayName.isEmpty ? author.username : author.displayName
|
||||
return L10n.Common.Controls.Status.userRepliedTo(name)
|
||||
}()
|
||||
cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.replyTo?.author.emojiDict ?? [:])
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: headerText, emojis: status.replyTo?.author.emojiMeta ?? [:])
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.statusView.headerInfoLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
cell.statusView.headerInfoLabel.reset()
|
||||
}
|
||||
cell.statusView.headerInfoLabel.accessibilityLabel = headerText
|
||||
cell.statusView.headerInfoLabel.isAccessibilityElement = status.replyTo != nil
|
||||
} else {
|
||||
|
@ -682,8 +694,15 @@ extension StatusSection {
|
|||
// name
|
||||
let author = (status.reblog ?? status).author
|
||||
let nameContent = author.displayNameWithFallback
|
||||
cell.statusView.nameLabel.configure(content: nameContent, emojiDict: author.emojiDict)
|
||||
cell.statusView.nameLabel.accessibilityLabel = nameContent
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: nameContent, emojis: author.emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.statusView.nameMetaLabel.configure(content: metaContent)
|
||||
cell.statusView.nameMetaLabel.accessibilityLabel = metaContent.trimmed
|
||||
} catch {
|
||||
cell.statusView.nameMetaLabel.reset()
|
||||
cell.statusView.nameMetaLabel.accessibilityLabel = ""
|
||||
}
|
||||
// username
|
||||
cell.statusView.usernameLabel.text = "@" + author.acct
|
||||
// avatar
|
||||
|
@ -1016,6 +1035,7 @@ extension StatusSection {
|
|||
}
|
||||
snapshot.appendItems(pollItems, toSection: .main)
|
||||
cell.statusView.pollTableViewDataSource?.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||
cell.statusView.pollTableViewHeightLayoutConstraint.constant = PollOptionTableViewCell.height * CGFloat(poll.options.count)
|
||||
}
|
||||
|
||||
static func configureActionToolBar(
|
||||
|
|
|
@ -1,173 +1,66 @@
|
|||
//extension ActiveEntity {
|
||||
//
|
||||
// ActiveLabel.swift
|
||||
// Mastodon
|
||||
// var accessibilityLabelDescription: String {
|
||||
// switch self.type {
|
||||
// case .email: return L10n.Common.Controls.Status.Tag.email
|
||||
// case .hashtag: return L10n.Common.Controls.Status.Tag.hashtag
|
||||
// case .mention: return L10n.Common.Controls.Status.Tag.mention
|
||||
// case .url: return L10n.Common.Controls.Status.Tag.url
|
||||
// case .emoji: return L10n.Common.Controls.Status.Tag.emoji
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Created by sxiaojian on 2021/1/29.
|
||||
// var accessibilityValueDescription: String {
|
||||
// switch self.type {
|
||||
// case .email(let text, _): return text
|
||||
// case .hashtag(let text, _): return text
|
||||
// case .mention(let text, _): return text
|
||||
// case .url(_, let trimmed, _, _): return trimmed
|
||||
// case .emoji(let text, _, _): return text
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func accessibilityElement(in accessibilityContainer: Any) -> ActiveLabelAccessibilityElement? {
|
||||
// if case .emoji = self.type {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// let element = ActiveLabelAccessibilityElement(accessibilityContainer: accessibilityContainer)
|
||||
// element.accessibilityTraits = .button
|
||||
// element.accessibilityLabel = accessibilityLabelDescription
|
||||
// element.accessibilityValue = accessibilityValueDescription
|
||||
// return element
|
||||
// }
|
||||
//}
|
||||
|
||||
import UIKit
|
||||
import Foundation
|
||||
import ActiveLabel
|
||||
import os.log
|
||||
import MastodonUI
|
||||
|
||||
extension ActiveLabel {
|
||||
|
||||
enum Style {
|
||||
case `default`
|
||||
case statusHeader
|
||||
case statusName
|
||||
case profileFieldName
|
||||
case profileFieldValue
|
||||
}
|
||||
|
||||
convenience init(style: Style) {
|
||||
self.init()
|
||||
|
||||
numberOfLines = 0
|
||||
lineSpacing = 5
|
||||
mentionColor = Asset.Colors.brandBlue.color
|
||||
hashtagColor = Asset.Colors.brandBlue.color
|
||||
URLColor = Asset.Colors.brandBlue.color
|
||||
emojiPlaceholderColor = .systemFill
|
||||
|
||||
accessibilityContainerType = .semanticGroup
|
||||
|
||||
switch style {
|
||||
case .default:
|
||||
font = .preferredFont(forTextStyle: .body)
|
||||
textColor = Asset.Colors.Label.primary.color
|
||||
case .statusHeader:
|
||||
font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium), maximumPointSize: 17)
|
||||
textColor = Asset.Colors.Label.secondary.color
|
||||
numberOfLines = 1
|
||||
case .statusName:
|
||||
font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
textColor = Asset.Colors.Label.primary.color
|
||||
numberOfLines = 1
|
||||
case .profileFieldName:
|
||||
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 20)
|
||||
textColor = Asset.Colors.Label.primary.color
|
||||
numberOfLines = 1
|
||||
case .profileFieldValue:
|
||||
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 20)
|
||||
textColor = Asset.Colors.Label.primary.color
|
||||
numberOfLines = 1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ActiveLabel {
|
||||
public func configure(text: String) {
|
||||
attributedText = nil
|
||||
activeEntities.removeAll()
|
||||
self.text = text
|
||||
accessibilityLabel = text
|
||||
}
|
||||
}
|
||||
|
||||
extension ActiveLabel {
|
||||
|
||||
/// status content
|
||||
public func configure(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
||||
attributedText = nil
|
||||
activeEntities.removeAll()
|
||||
|
||||
if let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict) {
|
||||
text = parseResult.trimmed
|
||||
activeEntities = parseResult.activeEntities
|
||||
accessibilityLabel = parseResult.original
|
||||
} else {
|
||||
text = ""
|
||||
accessibilityLabel = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func configure(contentParseResult parseResult: MastodonStatusContent.ParseResult?) {
|
||||
attributedText = nil
|
||||
activeEntities.removeAll()
|
||||
text = parseResult?.trimmed ?? ""
|
||||
activeEntities = parseResult?.activeEntities ?? []
|
||||
accessibilityLabel = parseResult?.original ?? nil
|
||||
}
|
||||
|
||||
/// account note
|
||||
public func configure(note: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
||||
configure(content: note, emojiDict: emojiDict)
|
||||
}
|
||||
}
|
||||
|
||||
extension ActiveLabel {
|
||||
/// account field
|
||||
public func configure(field: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
||||
configure(content: field, emojiDict: emojiDict)
|
||||
}
|
||||
}
|
||||
|
||||
extension ActiveEntity {
|
||||
|
||||
var accessibilityLabelDescription: String {
|
||||
switch self.type {
|
||||
case .email: return L10n.Common.Controls.Status.Tag.email
|
||||
case .hashtag: return L10n.Common.Controls.Status.Tag.hashtag
|
||||
case .mention: return L10n.Common.Controls.Status.Tag.mention
|
||||
case .url: return L10n.Common.Controls.Status.Tag.url
|
||||
case .emoji: return L10n.Common.Controls.Status.Tag.emoji
|
||||
}
|
||||
}
|
||||
|
||||
var accessibilityValueDescription: String {
|
||||
switch self.type {
|
||||
case .email(let text, _): return text
|
||||
case .hashtag(let text, _): return text
|
||||
case .mention(let text, _): return text
|
||||
case .url(_, let trimmed, _, _): return trimmed
|
||||
case .emoji(let text, _, _): return text
|
||||
}
|
||||
}
|
||||
|
||||
func accessibilityElement(in accessibilityContainer: Any) -> ActiveLabelAccessibilityElement? {
|
||||
if case .emoji = self.type {
|
||||
return nil
|
||||
}
|
||||
|
||||
let element = ActiveLabelAccessibilityElement(accessibilityContainer: accessibilityContainer)
|
||||
element.accessibilityTraits = .button
|
||||
element.accessibilityLabel = accessibilityLabelDescription
|
||||
element.accessibilityValue = accessibilityValueDescription
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
final class ActiveLabelAccessibilityElement: UIAccessibilityElement {
|
||||
var index: Int!
|
||||
}
|
||||
|
||||
//final class ActiveLabelAccessibilityElement: UIAccessibilityElement {
|
||||
// var index: Int!
|
||||
//}
|
||||
//
|
||||
// MARK: - UIAccessibilityContainer
|
||||
extension ActiveLabel {
|
||||
|
||||
func createAccessibilityElements() -> [UIAccessibilityElement] {
|
||||
var elements: [UIAccessibilityElement] = []
|
||||
|
||||
let element = ActiveLabelAccessibilityElement(accessibilityContainer: self)
|
||||
element.accessibilityTraits = .staticText
|
||||
element.accessibilityLabel = accessibilityLabel
|
||||
element.accessibilityFrame = superview!.convert(frame, to: nil)
|
||||
element.accessibilityLanguage = accessibilityLanguage
|
||||
elements.append(element)
|
||||
|
||||
for entity in activeEntities {
|
||||
guard let element = entity.accessibilityElement(in: self) else { continue }
|
||||
var glyphRange = NSRange()
|
||||
layoutManager.characterRange(forGlyphRange: entity.range, actualGlyphRange: &glyphRange)
|
||||
let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||
element.accessibilityFrame = self.convert(rect, to: nil)
|
||||
element.accessibilityContainer = self
|
||||
elements.append(element)
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
}
|
||||
//extension ActiveLabel {
|
||||
//
|
||||
// func createAccessibilityElements() -> [UIAccessibilityElement] {
|
||||
// var elements: [UIAccessibilityElement] = []
|
||||
//
|
||||
// let element = ActiveLabelAccessibilityElement(accessibilityContainer: self)
|
||||
// element.accessibilityTraits = .staticText
|
||||
// element.accessibilityLabel = accessibilityLabel
|
||||
// element.accessibilityFrame = superview!.convert(frame, to: nil)
|
||||
// element.accessibilityLanguage = accessibilityLanguage
|
||||
// elements.append(element)
|
||||
//
|
||||
// for entity in activeEntities {
|
||||
// guard let element = entity.accessibilityElement(in: self) else { continue }
|
||||
// var glyphRange = NSRange()
|
||||
// layoutManager.characterRange(forGlyphRange: entity.range, actualGlyphRange: &glyphRange)
|
||||
// let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
|
||||
// element.accessibilityFrame = self.convert(rect, to: nil)
|
||||
// element.accessibilityContainer = self
|
||||
// elements.append(element)
|
||||
// }
|
||||
//
|
||||
// return elements
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
|
|
@ -13,6 +13,8 @@ protocol EmojiContainer {
|
|||
var emojisData: Data? { get }
|
||||
}
|
||||
|
||||
// FIXME: `Mastodon.Entity.Account` extension
|
||||
|
||||
extension EmojiContainer {
|
||||
|
||||
static func encode(emojis: [Mastodon.Entity.Emoji]) -> Data? {
|
||||
|
@ -23,20 +25,13 @@ extension EmojiContainer {
|
|||
let decoder = JSONDecoder()
|
||||
return emojisData.flatMap { try? decoder.decode([Mastodon.Entity.Emoji].self, from: $0) }
|
||||
}
|
||||
|
||||
var emojiDict: MastodonStatusContent.EmojiDict {
|
||||
var dict = MastodonStatusContent.EmojiDict()
|
||||
for emoji in emojis ?? [] {
|
||||
guard let url = URL(string: emoji.url) else { continue }
|
||||
dict[emoji.shortcode] = url
|
||||
}
|
||||
return dict
|
||||
}
|
||||
|
||||
var emojiMeta: MastodonContent.Emojis {
|
||||
let isAnimated = !UserDefaults.shared.preferredStaticEmoji
|
||||
|
||||
var dict = MastodonContent.Emojis()
|
||||
for emoji in emojis ?? [] {
|
||||
dict[emoji.shortcode] = emoji.url
|
||||
dict[emoji.shortcode] = isAnimated ? emoji.url : emoji.staticURL
|
||||
}
|
||||
return dict
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
|
||||
extension Mastodon.Entity.Account: Hashable {
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
|
@ -28,3 +29,15 @@ extension Mastodon.Entity.Account {
|
|||
return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")!
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Account {
|
||||
var emojiMeta: MastodonContent.Emojis {
|
||||
let isAnimated = !UserDefaults.shared.preferredStaticEmoji
|
||||
|
||||
var dict = MastodonContent.Emojis()
|
||||
for emoji in emojis ?? [] {
|
||||
dict[emoji.shortcode] = isAnimated ? emoji.url : emoji.staticURL
|
||||
}
|
||||
return dict
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
//
|
||||
// MetaText.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-7-22.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Meta
|
||||
import MetaTextKit
|
||||
|
||||
extension MetaLabel {
|
||||
enum Style {
|
||||
case statusHeader
|
||||
case statusName
|
||||
case notificationName
|
||||
case profileFieldName
|
||||
case profileFieldValue
|
||||
case recommendAccountName
|
||||
case titleView
|
||||
case settingTableFooter
|
||||
}
|
||||
|
||||
convenience init(style: Style) {
|
||||
self.init()
|
||||
|
||||
layer.masksToBounds = true
|
||||
textContainer.lineBreakMode = .byTruncatingTail
|
||||
|
||||
let font: UIFont
|
||||
let textColor: UIColor
|
||||
|
||||
switch style {
|
||||
case .statusHeader:
|
||||
font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium), maximumPointSize: 17)
|
||||
textColor = Asset.Colors.Label.secondary.color
|
||||
|
||||
case .statusName:
|
||||
font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
textColor = Asset.Colors.Label.primary.color
|
||||
|
||||
case .notificationName:
|
||||
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20)
|
||||
textColor = Asset.Colors.brandBlue.color
|
||||
|
||||
case .profileFieldName:
|
||||
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 20)
|
||||
textColor = Asset.Colors.Label.primary.color
|
||||
|
||||
case .profileFieldValue:
|
||||
font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 20)
|
||||
textColor = Asset.Colors.Label.primary.color
|
||||
textAlignment = .right
|
||||
|
||||
|
||||
case .titleView:
|
||||
font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
textColor = Asset.Colors.Label.primary.color
|
||||
textAlignment = .center
|
||||
paragraphStyle.alignment = .center
|
||||
|
||||
case .recommendAccountName:
|
||||
font = .systemFont(ofSize: 18, weight: .semibold)
|
||||
textColor = .white
|
||||
|
||||
case .settingTableFooter:
|
||||
font = .preferredFont(forTextStyle: .body)
|
||||
textColor = Asset.Colors.Label.primary.color
|
||||
numberOfLines = 0
|
||||
textContainer.maximumNumberOfLines = 0
|
||||
paragraphStyle.alignment = .center
|
||||
}
|
||||
|
||||
self.font = font
|
||||
self.textColor = textColor
|
||||
|
||||
textAttributes = [
|
||||
.font: font,
|
||||
.foregroundColor: textColor
|
||||
]
|
||||
linkAttributes = [
|
||||
.font: font,
|
||||
.foregroundColor: Asset.Colors.brandBlue.color
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct PlaintextMetaContent: MetaContent {
|
||||
let string: String
|
||||
let entities: [Meta.Entity] = []
|
||||
|
||||
init(string: String) {
|
||||
self.string = string
|
||||
}
|
||||
|
||||
func metaAttachment(for entity: Meta.Entity) -> MetaAttachment? {
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -943,12 +943,6 @@ internal enum L10n {
|
|||
/// Appearance
|
||||
internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Appearance.Title")
|
||||
}
|
||||
internal enum AppearanceSettings {
|
||||
/// Disable animated avatars
|
||||
internal static let disableAvatarAnimation = L10n.tr("Localizable", "Scene.Settings.Section.AppearanceSettings.DisableAvatarAnimation")
|
||||
/// True black dark mode
|
||||
internal static let trueBlackDarkMode = L10n.tr("Localizable", "Scene.Settings.Section.AppearanceSettings.TrueBlackDarkMode")
|
||||
}
|
||||
internal enum BoringZone {
|
||||
/// Account Settings
|
||||
internal static let accountSettings = L10n.tr("Localizable", "Scene.Settings.Section.BoringZone.AccountSettings")
|
||||
|
@ -984,8 +978,14 @@ internal enum L10n {
|
|||
}
|
||||
}
|
||||
internal enum Preference {
|
||||
/// Disable animated avatars
|
||||
internal static let disableAvatarAnimation = L10n.tr("Localizable", "Scene.Settings.Section.Preference.DisableAvatarAnimation")
|
||||
/// Disable animated emojis
|
||||
internal static let disableEmojiAnimation = L10n.tr("Localizable", "Scene.Settings.Section.Preference.DisableEmojiAnimation")
|
||||
/// Preferences
|
||||
internal static let title = L10n.tr("Localizable", "Scene.Settings.Section.Preference.Title")
|
||||
/// True black dark mode
|
||||
internal static let trueBlackDarkMode = L10n.tr("Localizable", "Scene.Settings.Section.Preference.TrueBlackDarkMode")
|
||||
/// Use default browser to open links
|
||||
internal static let usingDefaultBrowser = L10n.tr("Localizable", "Scene.Settings.Section.Preference.UsingDefaultBrowser")
|
||||
}
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
//
|
||||
// MastodonField.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-30.
|
||||
//
|
||||
|
||||
//import Foundation
|
||||
//import ActiveLabel
|
||||
//
|
||||
//enum MastodonField {
|
||||
//
|
||||
// @available(*, deprecated, message: "rely on server meta rendering")
|
||||
// public static func parse(field string: String, emojiDict: MastodonStatusContent.EmojiDict) -> ParseResult {
|
||||
// // use content parser get emoji entities
|
||||
// let value = string
|
||||
//
|
||||
// var string = string
|
||||
// var entities: [ActiveEntity] = []
|
||||
//
|
||||
// do {
|
||||
// let contentParseresult = try MastodonStatusContent.parse(content: string, emojiDict: emojiDict)
|
||||
// string = contentParseresult.trimmed
|
||||
// entities.append(contentsOf: contentParseresult.activeEntities)
|
||||
// } catch {
|
||||
// // assertionFailure(error.localizedDescription)
|
||||
// }
|
||||
//
|
||||
// let mentionMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?)")
|
||||
// let hashtagMatches = string.matches(pattern: "(?:#([^\\s.]+))")
|
||||
// let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)")
|
||||
//
|
||||
//
|
||||
// for match in mentionMatches {
|
||||
// guard let text = string.substring(with: match, at: 0) else { continue }
|
||||
// let entity = ActiveEntity(range: match.range, type: .mention(text, userInfo: nil))
|
||||
// entities.append(entity)
|
||||
// }
|
||||
//
|
||||
// for match in hashtagMatches {
|
||||
// guard let text = string.substring(with: match, at: 0) else { continue }
|
||||
// let entity = ActiveEntity(range: match.range, type: .hashtag(text, userInfo: nil))
|
||||
// entities.append(entity)
|
||||
// }
|
||||
//
|
||||
// for match in urlMatches {
|
||||
// guard let text = string.substring(with: match, at: 0) else { continue }
|
||||
// let entity = ActiveEntity(range: match.range, type: .url(text, trimmed: text, url: text, userInfo: nil))
|
||||
// entities.append(entity)
|
||||
// }
|
||||
//
|
||||
// return ParseResult(value: value, trimmed: string, activeEntities: entities)
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
//extension MastodonField {
|
||||
// public struct ParseResult {
|
||||
// let value: String
|
||||
// let trimmed: String
|
||||
// let activeEntities: [ActiveEntity]
|
||||
// }
|
||||
//}
|
|
@ -1,17 +0,0 @@
|
|||
//
|
||||
// MastodonStatusContent+Appearance.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension MastodonStatusContent {
|
||||
public struct Appearance {
|
||||
let attributes: [NSAttributedString.Key: Any]
|
||||
let urlAttributes: [NSAttributedString.Key: Any]
|
||||
let hashtagAttributes: [NSAttributedString.Key: Any]
|
||||
let mentionAttributes: [NSAttributedString.Key: Any]
|
||||
}
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
//
|
||||
// MastodonStatusContent+ParseResult.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ActiveLabel
|
||||
|
||||
extension MastodonStatusContent {
|
||||
public struct ParseResult: Hashable {
|
||||
public let document: String
|
||||
public let original: String
|
||||
public let trimmed: String
|
||||
public let activeEntities: [ActiveEntity]
|
||||
|
||||
public static func == (lhs: MastodonStatusContent.ParseResult, rhs: MastodonStatusContent.ParseResult) -> Bool {
|
||||
return lhs.document == rhs.document
|
||||
&& lhs.original == rhs.original
|
||||
&& lhs.trimmed == rhs.trimmed
|
||||
&& lhs.activeEntities.count == rhs.activeEntities.count // FIXME:
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(document)
|
||||
hasher.combine(original)
|
||||
hasher.combine(trimmed)
|
||||
hasher.combine(activeEntities.count) // FIXME:
|
||||
}
|
||||
|
||||
func trimmedAttributedString(appearance: MastodonStatusContent.Appearance) -> NSAttributedString {
|
||||
let attributedString = NSMutableAttributedString(string: trimmed, attributes: appearance.attributes)
|
||||
for entity in activeEntities {
|
||||
switch entity.type {
|
||||
case .url:
|
||||
attributedString.addAttributes(appearance.urlAttributes, range: entity.range)
|
||||
case .hashtag:
|
||||
attributedString.addAttributes(appearance.hashtagAttributes, range: entity.range)
|
||||
case .mention:
|
||||
attributedString.addAttributes(appearance.mentionAttributes, range: entity.range)
|
||||
default:
|
||||
break
|
||||
}
|
||||
if let uri = entity.type.uri {
|
||||
attributedString.addAttributes([
|
||||
.link: uri
|
||||
], range: entity.range)
|
||||
}
|
||||
}
|
||||
return attributedString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ActiveEntityType {
|
||||
|
||||
static let appScheme = "mastodon"
|
||||
|
||||
public init?(url: URL) {
|
||||
guard let scheme = url.scheme?.lowercased() else { return nil }
|
||||
guard scheme == ActiveEntityType.appScheme else {
|
||||
self = .url("", trimmed: "", url: url.absoluteString, userInfo: nil)
|
||||
return
|
||||
}
|
||||
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||
let parameters = components.queryItems else { return nil }
|
||||
|
||||
if let hashtag = parameters.first(where: { $0.name == "hashtag" }), let encoded = hashtag.value, let value = String(base64Encoded: encoded) {
|
||||
self = .hashtag(value, userInfo: nil)
|
||||
return
|
||||
}
|
||||
if let mention = parameters.first(where: { $0.name == "mention" }), let encoded = mention.value, let value = String(base64Encoded: encoded) {
|
||||
self = .mention(value, userInfo: nil)
|
||||
return
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public var uri: URL? {
|
||||
switch self {
|
||||
case .url(_, _, let url, _):
|
||||
return URL(string: url)
|
||||
case .hashtag(let hashtag, _):
|
||||
return URL(string: "\(ActiveEntityType.appScheme)://meta?hashtag=\(hashtag.base64Encoded)")
|
||||
case .mention(let mention, _):
|
||||
return URL(string: "\(ActiveEntityType.appScheme)://meta?mention=\(mention.base64Encoded)")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension String {
|
||||
fileprivate var base64Encoded: String {
|
||||
return Data(self.utf8).base64EncodedString()
|
||||
}
|
||||
|
||||
init?(base64Encoded: String) {
|
||||
guard let data = Data(base64Encoded: base64Encoded),
|
||||
let string = String(data: data, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
self = string
|
||||
}
|
||||
}
|
|
@ -1,332 +0,0 @@
|
|||
//
|
||||
// MastodonStatusContent.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/2/1.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import ActiveLabel
|
||||
import Fuzi
|
||||
|
||||
public enum MastodonStatusContent {
|
||||
|
||||
public typealias EmojiShortcode = String
|
||||
public typealias EmojiDict = [EmojiShortcode: URL]
|
||||
|
||||
static let workingQueue = DispatchQueue(label: "org.joinmastodon.app.ActiveLabel.working-queue", qos: .userInteractive, attributes: .concurrent)
|
||||
|
||||
public static func parseResult(content: String, emojiDict: MastodonStatusContent.EmojiDict) -> AnyPublisher<MastodonStatusContent.ParseResult?, Never> {
|
||||
return Future { promise in
|
||||
self.workingQueue.async {
|
||||
let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict)
|
||||
promise(.success(parseResult))
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public static func parse(content: String, emojiDict: EmojiDict) throws -> MastodonStatusContent.ParseResult {
|
||||
let document: String = {
|
||||
var content = content.replacingOccurrences(of: "</p>", with: "</p>\r\n")
|
||||
for (shortcode, url) in emojiDict {
|
||||
let emojiNode = "<span class=\"emoji\" href=\"\(url.absoluteString)\">\(shortcode)</span>"
|
||||
let pattern = ":\(shortcode):"
|
||||
content = content.replacingOccurrences(of: pattern, with: emojiNode)
|
||||
}
|
||||
return content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}()
|
||||
let rootNode = try Node.parse(document: document)
|
||||
let text = String(rootNode.text)
|
||||
|
||||
var activeEntities: [ActiveEntity] = []
|
||||
let entities = MastodonStatusContent.Node.entities(in: rootNode)
|
||||
for entity in entities {
|
||||
let range = NSRange(entity.text.startIndex..<entity.text.endIndex, in: text)
|
||||
|
||||
switch entity.type {
|
||||
case .url:
|
||||
guard let href = entity.href else { continue }
|
||||
let text = String(entity.text)
|
||||
activeEntities.append(ActiveEntity(range: range, type: .url(text, trimmed: entity.hrefEllipsis ?? text, url: href, userInfo: nil)))
|
||||
case .hashtag:
|
||||
var userInfo: [AnyHashable: Any] = [:]
|
||||
entity.href.flatMap { href in
|
||||
userInfo["href"] = href
|
||||
}
|
||||
let hashtag = String(entity.text).deletingPrefix("#")
|
||||
activeEntities.append(ActiveEntity(range: range, type: .hashtag(hashtag, userInfo: userInfo)))
|
||||
case .mention:
|
||||
var userInfo: [AnyHashable: Any] = [:]
|
||||
entity.href.flatMap { href in
|
||||
userInfo["href"] = href
|
||||
}
|
||||
let mention = String(entity.text).deletingPrefix("@")
|
||||
activeEntities.append(ActiveEntity(range: range, type: .mention(mention, userInfo: userInfo)))
|
||||
case .emoji:
|
||||
var userInfo: [AnyHashable: Any] = [:]
|
||||
guard let href = entity.href else { continue }
|
||||
userInfo["href"] = href
|
||||
let emoji = String(entity.text)
|
||||
activeEntities.append(ActiveEntity(range: range, type: .emoji(emoji, url: href, userInfo: userInfo)))
|
||||
case .none:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var trimmed = text
|
||||
for activeEntity in activeEntities {
|
||||
MastodonStatusContent.trimEntity(status: &trimmed, activeEntity: activeEntity, activeEntities: activeEntities)
|
||||
}
|
||||
|
||||
return ParseResult(
|
||||
document: document,
|
||||
original: text,
|
||||
trimmed: trimmed,
|
||||
activeEntities: activeEntities
|
||||
)
|
||||
}
|
||||
|
||||
static func trimEntity(status: inout String, activeEntity: ActiveEntity, activeEntities: [ActiveEntity]) {
|
||||
let text: String
|
||||
let trimmed: String
|
||||
switch activeEntity.type {
|
||||
case .url(let _text, let _trimmed, _, _):
|
||||
text = _text
|
||||
trimmed = _trimmed
|
||||
case .emoji(let _text, _, _):
|
||||
text = _text
|
||||
trimmed = " "
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
guard let index = activeEntities.firstIndex(where: { $0.range == activeEntity.range }) else { return }
|
||||
guard let range = Range(activeEntity.range, in: status) else { return }
|
||||
status.replaceSubrange(range, with: trimmed)
|
||||
|
||||
let offset = trimmed.count - text.count
|
||||
activeEntity.range.length += offset
|
||||
|
||||
let moveActiveEntities = Array(activeEntities[index...].dropFirst())
|
||||
for moveActiveEntity in moveActiveEntities {
|
||||
moveActiveEntity.range.location += offset
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension String {
|
||||
// ref: https://www.hackingwithswift.com/example-code/strings/how-to-remove-a-prefix-from-a-string
|
||||
func deletingPrefix(_ prefix: String) -> String {
|
||||
guard self.hasPrefix(prefix) else { return self }
|
||||
return String(self.dropFirst(prefix.count))
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonStatusContent {
|
||||
|
||||
class Node {
|
||||
|
||||
let level: Int
|
||||
let type: Type?
|
||||
|
||||
// substring text
|
||||
let text: Substring
|
||||
|
||||
// range in parent String
|
||||
var range: Range<String.Index> {
|
||||
return text.startIndex..<text.endIndex
|
||||
}
|
||||
|
||||
let tagName: String?
|
||||
let attributes: [String : String]
|
||||
let href: String?
|
||||
let hrefEllipsis: String?
|
||||
|
||||
let children: [Node]
|
||||
|
||||
init(
|
||||
level: Int,
|
||||
text: Substring,
|
||||
tagName: String?,
|
||||
attributes: [String : String],
|
||||
href: String?,
|
||||
hrefEllipsis: String?,
|
||||
children: [Node]
|
||||
) {
|
||||
let _classNames: Set<String> = {
|
||||
guard let className = attributes["class"] else { return Set() }
|
||||
return Set(className.components(separatedBy: " "))
|
||||
}()
|
||||
let _type: Type? = {
|
||||
if tagName == "a" {
|
||||
if _classNames.contains("u-url") {
|
||||
return .mention
|
||||
}
|
||||
if _classNames.contains("hashtag") {
|
||||
return .hashtag
|
||||
}
|
||||
return .url
|
||||
} else {
|
||||
if _classNames.contains("emoji") {
|
||||
return .emoji
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
self.level = level
|
||||
self.type = _type
|
||||
self.text = text
|
||||
self.tagName = tagName
|
||||
self.attributes = attributes
|
||||
self.href = href
|
||||
self.hrefEllipsis = hrefEllipsis
|
||||
self.children = children
|
||||
}
|
||||
|
||||
static func parse(document: String) throws -> MastodonStatusContent.Node {
|
||||
let document = document.replacingOccurrences(of: "<br>|<br />", with: "\r\n", options: .regularExpression, range: nil)
|
||||
let html = try HTMLDocument(string: document)
|
||||
|
||||
let body = html.body ?? nil
|
||||
let text = body?.stringValue ?? ""
|
||||
let level = 0
|
||||
let children: [MastodonStatusContent.Node] = body.flatMap { body in
|
||||
return Node.parse(element: body, parentText: text[...], parentLevel: level + 1)
|
||||
} ?? []
|
||||
let node = Node(
|
||||
level: level,
|
||||
text: text[...],
|
||||
tagName: body?.tag,
|
||||
attributes: body?.attributes ?? [:],
|
||||
href: nil,
|
||||
hrefEllipsis: nil,
|
||||
children: children
|
||||
)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
static func parse(element: XMLElement, parentText: Substring, parentLevel: Int) -> [Node] {
|
||||
let parent = element
|
||||
let scanner = Scanner(string: String(parentText))
|
||||
scanner.charactersToBeSkipped = .none
|
||||
|
||||
var children: [Node] = []
|
||||
for _element in parent.children {
|
||||
let _text = _element.stringValue
|
||||
|
||||
// scan element text
|
||||
_ = scanner.scanUpToString(_text)
|
||||
let startIndexOffset = scanner.currentIndex.utf16Offset(in: scanner.string)
|
||||
guard scanner.scanString(_text) != nil else {
|
||||
assertionFailure()
|
||||
continue
|
||||
}
|
||||
let endIndexOffset = scanner.currentIndex.utf16Offset(in: scanner.string)
|
||||
|
||||
// locate substring
|
||||
let startIndex = parentText.utf16.index(parentText.utf16.startIndex, offsetBy: startIndexOffset)
|
||||
let endIndex = parentText.utf16.index(parentText.utf16.startIndex, offsetBy: endIndexOffset)
|
||||
let text = Substring(parentText.utf16[startIndex..<endIndex])
|
||||
|
||||
let href = _element["href"]
|
||||
let hrefEllipsis = href.flatMap { _ in _element.firstChild(css: ".ellipsis")?.stringValue }
|
||||
|
||||
let level = parentLevel + 1
|
||||
let node = Node(
|
||||
level: level,
|
||||
text: text,
|
||||
tagName: _element.tag,
|
||||
attributes: _element.attributes,
|
||||
href: href,
|
||||
hrefEllipsis: hrefEllipsis,
|
||||
children: Node.parse(element: _element, parentText: text, parentLevel: level + 1)
|
||||
)
|
||||
children.append(node)
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
static func collect(
|
||||
node: Node,
|
||||
where predicate: (Node) -> Bool
|
||||
) -> [Node] {
|
||||
var nodes: [Node] = []
|
||||
|
||||
if predicate(node) {
|
||||
nodes.append(node)
|
||||
}
|
||||
|
||||
for child in node.children {
|
||||
nodes.append(contentsOf: Node.collect(node: child, where: predicate))
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonStatusContent.Node {
|
||||
enum `Type` {
|
||||
case url
|
||||
case mention
|
||||
case hashtag
|
||||
case emoji
|
||||
}
|
||||
|
||||
static func entities(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {
|
||||
return MastodonStatusContent.Node.collect(node: node) { node in node.type != nil }
|
||||
}
|
||||
|
||||
static func hashtags(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {
|
||||
return MastodonStatusContent.Node.collect(node: node) { node in node.type == .hashtag }
|
||||
}
|
||||
|
||||
static func mentions(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {
|
||||
return MastodonStatusContent.Node.collect(node: node) { node in node.type == .mention }
|
||||
}
|
||||
|
||||
static func urls(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {
|
||||
return MastodonStatusContent.Node.collect(node: node) { node in node.type == .url }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonStatusContent.Node: CustomDebugStringConvertible {
|
||||
var debugDescription: String {
|
||||
let linkInfo: String = {
|
||||
switch (href, hrefEllipsis) {
|
||||
case (nil, nil):
|
||||
return ""
|
||||
case (let href, let hrefEllipsis):
|
||||
return "(\(href ?? "nil") - \(hrefEllipsis ?? "nil"))"
|
||||
}
|
||||
}()
|
||||
let classNamesInfo: String = {
|
||||
guard let className = attributes["class"] else { return "" }
|
||||
return "@[\(className)]"
|
||||
}()
|
||||
let nodeDescription = String(
|
||||
format: "<%@>%@%@: %@",
|
||||
tagName ?? "",
|
||||
classNamesInfo,
|
||||
linkInfo,
|
||||
String(text)
|
||||
)
|
||||
guard !children.isEmpty else {
|
||||
return nodeDescription
|
||||
}
|
||||
|
||||
let indent = Array(repeating: " ", count: level).joined()
|
||||
let childrenDescription = children
|
||||
.map { indent + $0.debugDescription }
|
||||
.joined(separator: "\n")
|
||||
|
||||
return nodeDescription + "\n" + childrenDescription
|
||||
}
|
||||
}
|
|
@ -26,4 +26,13 @@ extension UserDefaults {
|
|||
set { self[#function] = newValue }
|
||||
}
|
||||
|
||||
@objc dynamic var preferredStaticEmoji: Bool {
|
||||
get {
|
||||
// default false
|
||||
// without set register to profile timeline performance
|
||||
return bool(forKey: #function)
|
||||
}
|
||||
set { self[#function] = newValue }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,13 +8,10 @@
|
|||
#if ASDK
|
||||
|
||||
import Foundation
|
||||
import ActiveLabel
|
||||
|
||||
// MARK: - StatusViewDelegate
|
||||
extension StatusNodeDelegate where Self: StatusProvider {
|
||||
func statusNode(_ node: StatusNode, statusContentTextNode: ASMetaEditableTextNode, didSelectActiveEntityType type: ActiveEntityType) {
|
||||
StatusProviderFacade.responseToStatusActiveLabelAction(provider: self, node: node, didSelectActiveEntityType: type)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -11,9 +11,8 @@ import Combine
|
|||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import ActiveLabel
|
||||
import Meta
|
||||
import MetaTextView
|
||||
import MetaTextKit
|
||||
|
||||
// MARK: - StatusViewDelegate
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||
|
@ -25,10 +24,6 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, avatarImageViewDidPressed imageView: UIImageView) {
|
||||
StatusProviderFacade.coordinateToStatusAuthorProfileScene(for: .primary, provider: self, cell: cell)
|
||||
}
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||
StatusProviderFacade.responseToStatusActiveLabelAction(provider: self, cell: cell, activeLabel: activeLabel, didTapEntity: entity)
|
||||
}
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
|
||||
StatusProviderFacade.responseToStatusMetaTextAction(provider: self, cell: cell, metaText: metaText, didSelectMeta: meta)
|
||||
|
|
|
@ -11,9 +11,8 @@ import Combine
|
|||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import ActiveLabel
|
||||
import Meta
|
||||
import MetaTextView
|
||||
import MetaTextKit
|
||||
|
||||
#if ASDK
|
||||
import AsyncDisplayKit
|
||||
|
@ -125,35 +124,6 @@ extension StatusProviderFacade {
|
|||
}
|
||||
|
||||
extension StatusProviderFacade {
|
||||
|
||||
static func responseToStatusActiveLabelAction(provider: StatusProvider, cell: UITableViewCell, activeLabel: ActiveLabel, didTapEntity entity: ActiveEntity) {
|
||||
switch entity.type {
|
||||
case .url(_, _, let url, _),
|
||||
.mention(let url, _) where url.lowercased().hasPrefix("http"):
|
||||
// note:
|
||||
// some server mark the normal url as "u-url" class. :
|
||||
guard let url = URL(string: url) else { return }
|
||||
if let domain = provider.context.authenticationService.activeMastodonAuthenticationBox.value?.domain, url.host == domain,
|
||||
url.pathComponents.count >= 4,
|
||||
url.pathComponents[0] == "/",
|
||||
url.pathComponents[1] == "web",
|
||||
url.pathComponents[2] == "statuses" {
|
||||
let statusID = url.pathComponents[3]
|
||||
let threadViewModel = RemoteThreadViewModel(context: provider.context, statusID: statusID)
|
||||
provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show)
|
||||
} else {
|
||||
provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
||||
}
|
||||
case .hashtag(let text, _):
|
||||
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: text)
|
||||
provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: provider, transition: .show)
|
||||
case .mention(let text, let userInfo):
|
||||
let href = userInfo?["href"] as? String
|
||||
coordinateToStatusMentionProfileScene(for: .primary, provider: provider, cell: cell, mention: text, href: href)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
static func responseToStatusMetaTextAction(provider: StatusProvider, cell: UITableViewCell, metaText: MetaText, didSelectMeta meta: Meta) {
|
||||
switch meta {
|
||||
|
@ -185,31 +155,6 @@ extension StatusProviderFacade {
|
|||
}
|
||||
|
||||
#if ASDK
|
||||
static func responseToStatusActiveLabelAction(provider: StatusProvider, node: ASCellNode, didSelectActiveEntityType type: ActiveEntityType) {
|
||||
switch type {
|
||||
case .hashtag(let text, _):
|
||||
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: text)
|
||||
provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: provider, transition: .show)
|
||||
case .mention(let text, _):
|
||||
coordinateToStatusMentionProfileScene(for: .primary, provider: provider, node: node, mention: text)
|
||||
case .url(_, _, let url, _):
|
||||
guard let url = URL(string: url) else { return }
|
||||
if let domain = provider.context.authenticationService.activeMastodonAuthenticationBox.value?.domain, url.host == domain,
|
||||
url.pathComponents.count >= 4,
|
||||
url.pathComponents[0] == "/",
|
||||
url.pathComponents[1] == "web",
|
||||
url.pathComponents[2] == "statuses" {
|
||||
let statusID = url.pathComponents[3]
|
||||
let threadViewModel = RemoteThreadViewModel(context: provider.context, statusID: statusID)
|
||||
provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show)
|
||||
} else {
|
||||
provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private static func coordinateToStatusMentionProfileScene(for target: Target, provider: StatusProvider, node: ASCellNode, mention: String) {
|
||||
guard let status = provider.status(node: node, indexPath: nil) else { return }
|
||||
coordinateToStatusMentionProfileScene(for: target, provider: provider, status: status, mention: mention, href: nil)
|
||||
|
|
|
@ -321,8 +321,6 @@ any server.";
|
|||
"Scene.Settings.Section.Appearance.Dark" = "Always Dark";
|
||||
"Scene.Settings.Section.Appearance.Light" = "Always Light";
|
||||
"Scene.Settings.Section.Appearance.Title" = "Appearance";
|
||||
"Scene.Settings.Section.AppearanceSettings.DisableAvatarAnimation" = "Disable animated avatars";
|
||||
"Scene.Settings.Section.AppearanceSettings.TrueBlackDarkMode" = "True black dark mode";
|
||||
"Scene.Settings.Section.BoringZone.AccountSettings" = "Account Settings";
|
||||
"Scene.Settings.Section.BoringZone.Privacy" = "Privacy Policy";
|
||||
"Scene.Settings.Section.BoringZone.Terms" = "Terms of Service";
|
||||
|
@ -337,7 +335,10 @@ any server.";
|
|||
"Scene.Settings.Section.Notifications.Trigger.Follower" = "a follower";
|
||||
"Scene.Settings.Section.Notifications.Trigger.Noone" = "no one";
|
||||
"Scene.Settings.Section.Notifications.Trigger.Title" = "Notify me when";
|
||||
"Scene.Settings.Section.Preference.DisableAvatarAnimation" = "Disable animated avatars";
|
||||
"Scene.Settings.Section.Preference.DisableEmojiAnimation" = "Disable animated emojis";
|
||||
"Scene.Settings.Section.Preference.Title" = "Preferences";
|
||||
"Scene.Settings.Section.Preference.TrueBlackDarkMode" = "True black dark mode";
|
||||
"Scene.Settings.Section.Preference.UsingDefaultBrowser" = "Use default browser to open links";
|
||||
"Scene.Settings.Section.SpicyZone.Clear" = "Clear Media Cache";
|
||||
"Scene.Settings.Section.SpicyZone.Signout" = "Sign Out";
|
||||
|
|
|
@ -321,8 +321,6 @@ any server.";
|
|||
"Scene.Settings.Section.Appearance.Dark" = "Always Dark";
|
||||
"Scene.Settings.Section.Appearance.Light" = "Always Light";
|
||||
"Scene.Settings.Section.Appearance.Title" = "Appearance";
|
||||
"Scene.Settings.Section.AppearanceSettings.DisableAvatarAnimation" = "Disable animated avatars";
|
||||
"Scene.Settings.Section.AppearanceSettings.TrueBlackDarkMode" = "True black dark mode";
|
||||
"Scene.Settings.Section.BoringZone.AccountSettings" = "Account Settings";
|
||||
"Scene.Settings.Section.BoringZone.Privacy" = "Privacy Policy";
|
||||
"Scene.Settings.Section.BoringZone.Terms" = "Terms of Service";
|
||||
|
@ -337,7 +335,10 @@ any server.";
|
|||
"Scene.Settings.Section.Notifications.Trigger.Follower" = "a follower";
|
||||
"Scene.Settings.Section.Notifications.Trigger.Noone" = "no one";
|
||||
"Scene.Settings.Section.Notifications.Trigger.Title" = "Notify me when";
|
||||
"Scene.Settings.Section.Preference.DisableAvatarAnimation" = "Disable animated avatars";
|
||||
"Scene.Settings.Section.Preference.DisableEmojiAnimation" = "Disable animated emojis";
|
||||
"Scene.Settings.Section.Preference.Title" = "Preferences";
|
||||
"Scene.Settings.Section.Preference.TrueBlackDarkMode" = "True black dark mode";
|
||||
"Scene.Settings.Section.Preference.UsingDefaultBrowser" = "Use default browser to open links";
|
||||
"Scene.Settings.Section.SpicyZone.Clear" = "Clear Media Cache";
|
||||
"Scene.Settings.Section.SpicyZone.Signout" = "Sign Out";
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import MetaTextView
|
||||
import MetaTextKit
|
||||
import UITextView_Placeholder
|
||||
|
||||
final class ComposeStatusContentTableViewCell: UITableViewCell {
|
||||
|
@ -40,20 +40,19 @@ final class ComposeStatusContentTableViewCell: UITableViewCell {
|
|||
attributes: attributes
|
||||
)
|
||||
}()
|
||||
let paragraphStyle: NSMutableParagraphStyle = {
|
||||
metaText.paragraphStyle = {
|
||||
let style = NSMutableParagraphStyle()
|
||||
style.lineSpacing = 5
|
||||
style.paragraphSpacing = 8
|
||||
return style
|
||||
}()
|
||||
metaText.textAttributes = [
|
||||
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)),
|
||||
.foregroundColor: Asset.Colors.Label.primary.color,
|
||||
.paragraphStyle: paragraphStyle,
|
||||
]
|
||||
metaText.linkAttributes = [
|
||||
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)),
|
||||
.foregroundColor: Asset.Colors.brandBlue.color,
|
||||
.paragraphStyle: paragraphStyle,
|
||||
]
|
||||
return metaText
|
||||
}()
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import FLAnimatedImage
|
||||
import SDWebImage
|
||||
|
||||
final class CustomEmojiPickerItemCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
static let itemSize = CGSize(width: 44, height: 44)
|
||||
|
||||
let emojiImageView: FLAnimatedImageView = {
|
||||
let imageView = FLAnimatedImageView()
|
||||
let emojiImageView: SDAnimatedImageView = {
|
||||
let imageView = SDAnimatedImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.layer.masksToBounds = true
|
||||
return imageView
|
||||
|
|
|
@ -10,7 +10,7 @@ import UIKit
|
|||
import Combine
|
||||
import PhotosUI
|
||||
import MastodonSDK
|
||||
import MetaTextView
|
||||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
import Meta
|
||||
import MastodonUI
|
||||
|
|
|
@ -9,10 +9,9 @@ import os.log
|
|||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import TwitterTextEditor
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
import MetaTextView
|
||||
import MetaTextKit
|
||||
|
||||
extension ComposeViewModel {
|
||||
|
||||
|
@ -193,8 +192,15 @@ extension ComposeViewModel: UITableViewDataSource {
|
|||
|
||||
// set avatar
|
||||
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
|
||||
// set name username
|
||||
cell.statusView.nameLabel.configure(content: status.author.displayNameWithFallback, emojiDict: status.author.emojiDict)
|
||||
// set name, username
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: status.author.displayNameWithFallback, emojis: status.author.emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.statusView.nameLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let metaContent = PlaintextMetaContent(string: status.author.displayNameWithFallback)
|
||||
cell.statusView.nameLabel.configure(content: metaContent)
|
||||
}
|
||||
cell.statusView.usernameLabel.text = "@" + status.author.acct
|
||||
// set text
|
||||
let content = MastodonContent(content: status.content, emojis: status.emojiMeta)
|
||||
|
@ -226,13 +232,14 @@ extension ComposeViewModel: UITableViewDataSource {
|
|||
let name = author.displayName.isEmpty ? author.username : author.displayName
|
||||
return L10n.Scene.Compose.replyingToUser(name)
|
||||
}()
|
||||
MastodonStatusContent.parseResult(content: headerText, emojiDict: replyTo.author.emojiDict)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell] parseResult in
|
||||
guard let cell = cell else { return }
|
||||
cell.statusView.headerInfoLabel.configure(contentParseResult: parseResult)
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: headerText, emojis: replyTo.author.emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
cell.statusView.headerInfoLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let metaContent = PlaintextMetaContent(string: headerText)
|
||||
cell.statusView.headerInfoLabel.configure(content: metaContent)
|
||||
}
|
||||
}
|
||||
// configure author
|
||||
ComposeStatusSection.configureStatusContent(cell: cell, attribute: composeStatusAttribute)
|
||||
|
|
|
@ -181,7 +181,7 @@ final class ComposeViewModel: NSObject {
|
|||
}
|
||||
return displayName
|
||||
}()
|
||||
self.composeStatusAttribute.emojiDict.value = mastodonUser?.emojiDict ?? [:]
|
||||
self.composeStatusAttribute.emojiMeta.value = mastodonUser?.emojiMeta ?? [:]
|
||||
self.composeStatusAttribute.username.value = username
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import UIKit
|
||||
import Combine
|
||||
import MetaTextView
|
||||
import MetaTextKit
|
||||
|
||||
final class CustomEmojiPickerInputViewModel {
|
||||
|
||||
|
|
|
@ -7,9 +7,8 @@
|
|||
|
||||
import os.log
|
||||
import UIKit
|
||||
import ActiveLabel
|
||||
import FLAnimatedImage
|
||||
import MetaTextView
|
||||
import MetaTextKit
|
||||
|
||||
final class ReplicaStatusView: UIView {
|
||||
|
||||
|
@ -52,12 +51,7 @@ final class ReplicaStatusView: UIView {
|
|||
return label
|
||||
}()
|
||||
|
||||
let headerInfoLabel: ActiveLabel = {
|
||||
let label = ActiveLabel(style: .statusHeader)
|
||||
label.text = "Bob reblogged"
|
||||
label.layer.masksToBounds = false
|
||||
return label
|
||||
}()
|
||||
let headerInfoLabel = MetaLabel(style: .statusHeader)
|
||||
|
||||
let avatarView: UIView = {
|
||||
let view = UIView()
|
||||
|
@ -68,10 +62,7 @@ final class ReplicaStatusView: UIView {
|
|||
}()
|
||||
let avatarImageView = FLAnimatedImageView()
|
||||
|
||||
let nameLabel: ActiveLabel = {
|
||||
let label = ActiveLabel(style: .statusName)
|
||||
return label
|
||||
}()
|
||||
let nameLabel = MetaLabel(style: .statusName)
|
||||
|
||||
let nameTrialingDotLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
|
|
|
@ -56,7 +56,7 @@ extension HashtagTimelineViewController {
|
|||
super.viewDidLoad()
|
||||
|
||||
title = "#\(viewModel.hashtag)"
|
||||
titleView.update(title: viewModel.hashtag, subtitle: nil, emojiDict: [:])
|
||||
titleView.update(title: viewModel.hashtag, subtitle: nil)
|
||||
navigationItem.titleView = titleView
|
||||
|
||||
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
|
||||
|
@ -150,7 +150,7 @@ extension HashtagTimelineViewController {
|
|||
private func updatePromptTitle() {
|
||||
var subtitle: String?
|
||||
defer {
|
||||
titleView.update(title: "#" + viewModel.hashtag, subtitle: subtitle, emojiDict: [:])
|
||||
titleView.update(title: "#" + viewModel.hashtag, subtitle: subtitle)
|
||||
}
|
||||
guard let histories = viewModel.hashtagEntity.value?.history else {
|
||||
return
|
||||
|
@ -209,7 +209,7 @@ extension HashtagTimelineViewController: LoadMoreConfigurableTableViewContainer
|
|||
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
|
||||
typealias LoadingState = HashtagTimelineViewModel.LoadOldestState.Loading
|
||||
var loadMoreConfigurableTableView: UITableView { return tableView }
|
||||
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine }
|
||||
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadOldestStateMachine }
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
|
|
|
@ -84,7 +84,7 @@ extension HashtagTimelineViewModel {
|
|||
newSnapshot.appendItems(statusItemList, toSection: .main)
|
||||
}
|
||||
|
||||
if !(self.loadoldestStateMachine.currentState is LoadOldestState.NoMore) {
|
||||
if !(self.loadOldestStateMachine.currentState is LoadOldestState.NoMore) {
|
||||
newSnapshot.appendItems([.bottomLoader], toSection: .main)
|
||||
}
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ final class HashtagTimelineViewModel: NSObject {
|
|||
}()
|
||||
lazy var loadLatestStateMachinePublisher = CurrentValueSubject<LoadLatestState?, Never>(nil)
|
||||
// bottom loader
|
||||
private(set) lazy var loadoldestStateMachine: GKStateMachine = {
|
||||
private(set) lazy var loadOldestStateMachine: GKStateMachine = {
|
||||
// exclude timeline middle fetcher state
|
||||
let stateMachine = GKStateMachine(states: [
|
||||
LoadOldestState.Initial(viewModel: self),
|
||||
|
|
|
@ -109,6 +109,12 @@ extension AsyncHomeTimelineViewController: StatusProvider {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func statusObjectItems(indexPaths: [IndexPath]) -> [StatusObjectItem] {
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else { return [] }
|
||||
let items = indexPaths.compactMap { diffableDataSource.itemIdentifier(for: $0)?.statusObjectItem }
|
||||
return items
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ extension AsyncHomeTimelineViewController {
|
|||
node.allowsSelection = true
|
||||
|
||||
title = L10n.Scene.HomeTimeline.title
|
||||
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
|
||||
navigationItem.leftBarButtonItem = settingBarButtonItem
|
||||
navigationItem.titleView = titleView
|
||||
titleView.delegate = self
|
||||
|
@ -341,7 +341,7 @@ extension AsyncHomeTimelineViewController {
|
|||
// typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
|
||||
// typealias LoadingState = HomeTimelineViewModel.LoadOldestState.Loading
|
||||
// var loadMoreConfigurableTableView: UITableView { return tableView }
|
||||
// var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine }
|
||||
// var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadOldestStateMachine }
|
||||
//}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
|
@ -556,7 +556,7 @@ extension AsyncHomeTimelineViewController: ASTableDelegate {
|
|||
}
|
||||
|
||||
func tableNode(_ tableNode: ASTableNode, willBeginBatchFetchWith context: ASBatchContext) {
|
||||
viewModel.loadoldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self)
|
||||
viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self)
|
||||
context.completeBatchFetching(true)
|
||||
}
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ extension AsyncHomeTimelineViewModel: NSFetchedResultsControllerDelegate {
|
|||
|
||||
let endSnapshot = CACurrentMediaTime()
|
||||
|
||||
if shouldAddBottomLoader, !(self.loadoldestStateMachine.currentState is LoadOldestState.NoMore) {
|
||||
if shouldAddBottomLoader, !(self.loadLatestStateMachine.currentState is LoadOldestState.NoMore) {
|
||||
newSnapshot.appendItems([.bottomLoader], toSection: .main)
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ import CoreDataStack
|
|||
import GameplayKit
|
||||
import AlamofireImage
|
||||
import DateToolsSwift
|
||||
import ActiveLabel
|
||||
import AsyncDisplayKit
|
||||
|
||||
final class AsyncHomeTimelineViewModel: NSObject {
|
||||
|
@ -59,7 +58,7 @@ final class AsyncHomeTimelineViewModel: NSObject {
|
|||
}()
|
||||
lazy var loadLatestStateMachinePublisher = CurrentValueSubject<LoadLatestState?, Never>(nil)
|
||||
// bottom loader
|
||||
private(set) lazy var loadoldestStateMachine: GKStateMachine = {
|
||||
private(set) lazy var loadOldestStateMachine: GKStateMachine = {
|
||||
// exclude timeline middle fetcher state
|
||||
let stateMachine = GKStateMachine(states: [
|
||||
LoadOldestState.Initial(viewModel: self),
|
||||
|
|
|
@ -388,7 +388,7 @@ extension HomeTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
|||
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
|
||||
typealias LoadingState = HomeTimelineViewModel.LoadOldestState.Loading
|
||||
var loadMoreConfigurableTableView: UITableView { return tableView }
|
||||
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine }
|
||||
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadLatestStateMachine }
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
|
|
|
@ -115,7 +115,7 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
|
|||
let endSnapshot = CACurrentMediaTime()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if shouldAddBottomLoader, !(self.loadoldestStateMachine.currentState is LoadOldestState.NoMore) {
|
||||
if shouldAddBottomLoader, !(self.loadLatestStateMachine.currentState is LoadOldestState.NoMore) {
|
||||
newSnapshot.appendItems([.bottomLoader], toSection: .main)
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ import CoreDataStack
|
|||
import GameplayKit
|
||||
import AlamofireImage
|
||||
import DateToolsSwift
|
||||
import ActiveLabel
|
||||
|
||||
final class HomeTimelineViewModel: NSObject {
|
||||
|
||||
|
@ -52,7 +51,7 @@ final class HomeTimelineViewModel: NSObject {
|
|||
}()
|
||||
lazy var loadLatestStateMachinePublisher = CurrentValueSubject<LoadLatestState?, Never>(nil)
|
||||
// bottom loader
|
||||
private(set) lazy var loadoldestStateMachine: GKStateMachine = {
|
||||
private(set) lazy var loadOldestStateMachine: GKStateMachine = {
|
||||
// exclude timeline middle fetcher state
|
||||
let stateMachine = GKStateMachine(states: [
|
||||
LoadOldestState.Initial(viewModel: self),
|
||||
|
|
|
@ -12,9 +12,8 @@ import GameplayKit
|
|||
import MastodonSDK
|
||||
import OSLog
|
||||
import UIKit
|
||||
import ActiveLabel
|
||||
import Meta
|
||||
import MetaTextView
|
||||
import MetaTextKit
|
||||
|
||||
final class NotificationViewController: UIViewController, NeedsDependency {
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
|
@ -272,7 +271,7 @@ extension NotificationViewController {
|
|||
switch item {
|
||||
case .bottomLoader:
|
||||
if !tableView.isDragging, !tableView.isDecelerating {
|
||||
viewModel.loadoldestStateMachine.enter(NotificationViewModel.LoadOldestState.Loading.self)
|
||||
viewModel.loadOldestStateMachine.enter(NotificationViewModel.LoadOldestState.Loading.self)
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
@ -305,7 +304,7 @@ extension NotificationViewController: NotificationTableViewCellDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, authorNameLabelDidPressed label: ActiveLabel) {
|
||||
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, authorNameLabelDidPressed label: MetaLabel) {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
guard let indexPath = tableView.indexPath(for: cell) else { return }
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
|
@ -319,7 +318,6 @@ extension NotificationViewController: NotificationTableViewCellDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
func notificationTableViewCell(_ cell: NotificationStatusTableViewCell, notification: MastodonNotification, acceptButtonDidPressed button: UIButton) {
|
||||
viewModel.acceptFollowRequest(notification: notification)
|
||||
}
|
||||
|
@ -380,7 +378,7 @@ extension NotificationViewController: LoadMoreConfigurableTableViewContainer {
|
|||
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
|
||||
typealias LoadingState = NotificationViewModel.LoadOldestState.Loading
|
||||
var loadMoreConfigurableTableView: UITableView { tableView }
|
||||
var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadoldestStateMachine }
|
||||
var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadOldestStateMachine }
|
||||
}
|
||||
|
||||
extension NotificationViewController {
|
||||
|
|
|
@ -54,7 +54,7 @@ final class NotificationViewModel: NSObject {
|
|||
lazy var loadLatestStateMachinePublisher = CurrentValueSubject<LoadLatestState?, Never>(nil)
|
||||
|
||||
// bottom loader
|
||||
private(set) lazy var loadoldestStateMachine: GKStateMachine = {
|
||||
private(set) lazy var loadOldestStateMachine: GKStateMachine = {
|
||||
// exclude timeline middle fetcher state
|
||||
let stateMachine = GKStateMachine(states: [
|
||||
LoadOldestState.Initial(viewModel: self),
|
||||
|
|
|
@ -10,8 +10,7 @@ import Combine
|
|||
import Foundation
|
||||
import CoreDataStack
|
||||
import UIKit
|
||||
import ActiveLabel
|
||||
import MetaTextView
|
||||
import MetaTextKit
|
||||
import Meta
|
||||
import FLAnimatedImage
|
||||
|
||||
|
@ -20,7 +19,7 @@ protocol NotificationTableViewCellDelegate: AnyObject {
|
|||
func parent() -> UIViewController
|
||||
|
||||
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, avatarImageViewDidPressed imageView: UIImageView)
|
||||
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, authorNameLabelDidPressed label: ActiveLabel)
|
||||
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, authorNameLabelDidPressed label: MetaLabel)
|
||||
|
||||
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton)
|
||||
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||
|
@ -58,13 +57,7 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
|
|||
return label
|
||||
}()
|
||||
|
||||
let nameLabel: ActiveLabel = {
|
||||
let label = ActiveLabel(style: .statusName)
|
||||
label.textColor = Asset.Colors.brandBlue.color
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20)
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
return label
|
||||
}()
|
||||
let nameLabel = MetaLabel(style: .notificationName)
|
||||
|
||||
let buttonStackView = UIStackView()
|
||||
|
||||
|
@ -318,10 +311,6 @@ extension NotificationStatusTableViewCell: StatusViewDelegate {
|
|||
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
|
||||
delegate?.notificationStatusTableViewCell(self, statusView: statusView, metaText: metaText, didSelectMeta: meta)
|
||||
|
|
|
@ -57,7 +57,7 @@ extension FavoriteViewController {
|
|||
.store(in: &disposeBag)
|
||||
|
||||
navigationItem.titleView = titleView
|
||||
titleView.update(title: L10n.Scene.Favorite.title, subtitle: nil, emojiDict: [:])
|
||||
titleView.update(title: L10n.Scene.Favorite.title, subtitle: nil)
|
||||
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(tableView)
|
||||
|
|
|
@ -9,16 +9,15 @@ import os.log
|
|||
import UIKit
|
||||
import Combine
|
||||
import PhotosUI
|
||||
import ActiveLabel
|
||||
import AlamofireImage
|
||||
import CropViewController
|
||||
import TwitterTextEditor
|
||||
import MastodonMeta
|
||||
import MetaTextKit
|
||||
|
||||
protocol ProfileHeaderViewControllerDelegate: AnyObject {
|
||||
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView)
|
||||
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, pageSegmentedControlValueChanged segmentedControl: UISegmentedControl, selectedSegmentIndex index: Int)
|
||||
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity)
|
||||
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta)
|
||||
}
|
||||
|
||||
final class ProfileHeaderViewController: UIViewController {
|
||||
|
@ -35,6 +34,7 @@ final class ProfileHeaderViewController: UIViewController {
|
|||
let titleView: DoubleTitleLabelNavigationBarTitleView = {
|
||||
let titleView = DoubleTitleLabelNavigationBarTitleView()
|
||||
titleView.titleLabel.textColor = .white
|
||||
titleView.titleLabel.textAttributes[.foregroundColor] = UIColor.white
|
||||
titleView.titleLabel.alpha = 0
|
||||
titleView.subtitleLabel.textColor = .white
|
||||
titleView.subtitleLabel.alpha = 0
|
||||
|
@ -179,19 +179,14 @@ extension ProfileHeaderViewController {
|
|||
viewModel.isEditing,
|
||||
viewModel.displayProfileInfo.name.removeDuplicates(),
|
||||
viewModel.editProfileInfo.name.removeDuplicates(),
|
||||
viewModel.emojiDict
|
||||
viewModel.emojiMeta
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isEditing, name, editingName, emojiDict in
|
||||
.sink { [weak self] isEditing, name, editingName, emojiMeta in
|
||||
guard let self = self else { return }
|
||||
do {
|
||||
var emojis = MastodonContent.Emojis()
|
||||
for (key, value) in emojiDict {
|
||||
emojis[key] = value.absoluteString
|
||||
}
|
||||
let metaContent = try MastodonMetaContent.convert(
|
||||
document: MastodonContent(content: name ?? " ", emojis: emojis)
|
||||
)
|
||||
let mastodonContent = MastodonContent(content: name ?? " ", emojis: emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
self.profileHeaderView.nameMetaText.configure(content: metaContent)
|
||||
} catch {
|
||||
assertionFailure()
|
||||
|
@ -200,25 +195,37 @@ extension ProfileHeaderViewController {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest3(
|
||||
viewModel.isEditing.eraseToAnyPublisher(),
|
||||
viewModel.displayProfileInfo.note.removeDuplicates().eraseToAnyPublisher(),
|
||||
viewModel.editProfileInfo.note.removeDuplicates().eraseToAnyPublisher()
|
||||
Publishers.CombineLatest4(
|
||||
viewModel.isEditing.removeDuplicates(),
|
||||
viewModel.displayProfileInfo.note.removeDuplicates(),
|
||||
viewModel.editProfileInfo.note.removeDuplicates(),
|
||||
viewModel.emojiMeta.removeDuplicates()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isEditing, note, editingNote in
|
||||
.sink { [weak self] isEditing, note, editingNote, emojiMeta in
|
||||
guard let self = self else { return }
|
||||
self.profileHeaderView.bioActiveLabel.configure(note: note ?? "", emojiDict: [:]) // FIXME: custom emoji
|
||||
|
||||
// prevent duplicate set
|
||||
let editingNote = editingNote ?? ""
|
||||
if self.profileHeaderView.bioTextEditorView.text != editingNote {
|
||||
self.profileHeaderView.bioTextEditorView.text = editingNote
|
||||
self.profileHeaderView.bioMetaText.textView.isEditable = isEditing
|
||||
|
||||
if isEditing {
|
||||
if self.profileHeaderView.bioMetaText.backedString != note {
|
||||
let metaContent = PlaintextMetaContent(string: editingNote ?? "")
|
||||
self.profileHeaderView.bioMetaText.configure(content: metaContent)
|
||||
}
|
||||
} else {
|
||||
let mastodonContent = MastodonContent(content: note ?? "", emojis: emojiMeta)
|
||||
do {
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
self.profileHeaderView.bioMetaText.configure(content: metaContent)
|
||||
} catch {
|
||||
assertionFailure()
|
||||
self.profileHeaderView.bioMetaText.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
profileHeaderView.bioTextEditorView.changeObserver = self
|
||||
profileHeaderView.bioMetaText.delegate = self
|
||||
|
||||
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: profileHeaderView.nameTextField)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] notification in
|
||||
|
@ -450,13 +457,16 @@ extension ProfileHeaderViewController {
|
|||
|
||||
}
|
||||
|
||||
// MARK: - TextEditorViewChangeObserver
|
||||
extension ProfileHeaderViewController: TextEditorViewChangeObserver {
|
||||
func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, textEditorView.text)
|
||||
guard changeResult.isTextChanged else { return }
|
||||
assert(textEditorView === profileHeaderView.bioTextEditorView)
|
||||
viewModel.editProfileInfo.note.value = textEditorView.text
|
||||
// MARK: - MetaTextDelegate
|
||||
extension ProfileHeaderViewController: MetaTextDelegate {
|
||||
func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, metaText.backedString)
|
||||
assert(metaText.textView === profileHeaderView.bioMetaText.textView)
|
||||
if metaText.textView === profileHeaderView.bioMetaText.textView {
|
||||
viewModel.editProfileInfo.note.value = metaText.backedString
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -533,6 +543,7 @@ extension ProfileHeaderViewController: UICollectionViewDelegate {
|
|||
|
||||
// MARK: - ProfileFieldCollectionViewCellDelegate
|
||||
extension ProfileHeaderViewController: ProfileFieldCollectionViewCellDelegate {
|
||||
|
||||
// should be remove style edit button
|
||||
func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, editButtonDidPressed button: UIButton) {
|
||||
guard let diffableDataSource = viewModel.fieldDiffableDataSource else { return }
|
||||
|
@ -541,8 +552,8 @@ extension ProfileHeaderViewController: ProfileFieldCollectionViewCellDelegate {
|
|||
viewModel.removeFieldItem(item: item)
|
||||
}
|
||||
|
||||
func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||
delegate?.profileHeaderViewController(self, profileFieldCollectionViewCell: cell, activeLabel: activeLabel, didSelectActiveEntity: entity)
|
||||
func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, metaLebel: MetaLabel, didSelectMeta meta: Meta) {
|
||||
delegate?.profileHeaderViewController(self, profileFieldCollectionViewCell: cell, metaLabel: metaLebel, didSelectMeta: meta)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
|||
import Combine
|
||||
import Kanna
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
|
||||
final class ProfileHeaderViewModel {
|
||||
|
||||
|
@ -24,7 +25,7 @@ final class ProfileHeaderViewModel {
|
|||
let needsSetupBottomShadow = CurrentValueSubject<Bool, Never>(true)
|
||||
let needsFiledCollectionViewHidden = CurrentValueSubject<Bool, Never>(false)
|
||||
let isTitleViewContentOffsetSet = CurrentValueSubject<Bool, Never>(false)
|
||||
let emojiDict = CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>([:])
|
||||
let emojiMeta = CurrentValueSubject<MastodonContent.Emojis, Never>([:])
|
||||
let accountForEdit = CurrentValueSubject<Mastodon.Entity.Account?, Never>(nil)
|
||||
|
||||
// output
|
||||
|
@ -58,10 +59,10 @@ final class ProfileHeaderViewModel {
|
|||
isEditing.removeDuplicates(),
|
||||
displayProfileInfo.fields.removeDuplicates(),
|
||||
editProfileInfo.fields.removeDuplicates(),
|
||||
emojiDict.removeDuplicates()
|
||||
emojiMeta.removeDuplicates()
|
||||
)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] isEditing, displayFields, editingFields, emojiDict in
|
||||
.sink { [weak self] isEditing, displayFields, editingFields, emojiMeta in
|
||||
guard let self = self else { return }
|
||||
guard let diffableDataSource = self.fieldDiffableDataSource else { return }
|
||||
|
||||
|
@ -87,7 +88,7 @@ final class ProfileHeaderViewModel {
|
|||
|
||||
let attribute = oldFieldAttributeDict[field.id] ?? ProfileFieldItem.FieldItemAttribute()
|
||||
attribute.isEditing = isEditing
|
||||
attribute.emojiDict.value = emojiDict
|
||||
attribute.emojiMeta.value = emojiMeta
|
||||
attribute.isLast = false
|
||||
return ProfileFieldItem.field(field: field, attribute: attribute)
|
||||
}
|
||||
|
|
|
@ -95,12 +95,12 @@ extension ProfileFieldAddEntryCollectionViewCell {
|
|||
bottomSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
fieldView.titleActiveLabel.isHidden = false
|
||||
fieldView.titleActiveLabel.configure(field: L10n.Scene.Profile.Fields.addRow, emojiDict: [:])
|
||||
fieldView.titleMetaLabel.isHidden = false
|
||||
fieldView.titleMetaLabel.configure(content: PlaintextMetaContent(string: L10n.Scene.Profile.Fields.addRow))
|
||||
fieldView.titleTextField.isHidden = true
|
||||
|
||||
fieldView.valueActiveLabel.isHidden = false
|
||||
fieldView.valueActiveLabel.configure(field: " ", emojiDict: [:])
|
||||
fieldView.valueMetaLabel.isHidden = false
|
||||
fieldView.valueMetaLabel.configure(content: PlaintextMetaContent(string: " "))
|
||||
fieldView.valueTextField.isHidden = true
|
||||
|
||||
addGestureRecognizer(singleTagGestureRecognizer)
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import ActiveLabel
|
||||
import MetaTextKit
|
||||
|
||||
protocol ProfileFieldCollectionViewCellDelegate: AnyObject {
|
||||
func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, editButtonDidPressed button: UIButton)
|
||||
func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity)
|
||||
func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, metaLebel: MetaLabel, didSelectMeta meta: Meta)
|
||||
}
|
||||
|
||||
final class ProfileFieldCollectionViewCell: UICollectionViewCell {
|
||||
|
@ -107,7 +107,7 @@ extension ProfileFieldCollectionViewCell {
|
|||
|
||||
editButton.addTarget(self, action: #selector(ProfileFieldCollectionViewCell.editButtonDidPressed(_:)), for: .touchUpInside)
|
||||
|
||||
fieldView.valueActiveLabel.delegate = self
|
||||
fieldView.valueMetaLabel.linkDelegate = self
|
||||
|
||||
resetSeparatorLineLayout()
|
||||
}
|
||||
|
@ -153,13 +153,11 @@ extension ProfileFieldCollectionViewCell {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - ActiveLabelDelegate
|
||||
extension ProfileFieldCollectionViewCell: ActiveLabelDelegate {
|
||||
func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||
// MARK: - MetaLabelDelegate
|
||||
extension ProfileFieldCollectionViewCell: MetaLabelDelegate {
|
||||
func metaLabel(_ metaLabel: MetaLabel, didSelectMeta meta: Meta) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
delegate?.profileFieldCollectionViewCell(self, activeLabel: activeLabel, didSelectActiveEntity: entity)
|
||||
delegate?.profileFieldCollectionViewCell(self, metaLebel: metaLabel, didSelectMeta: meta)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import UIKit
|
||||
import Combine
|
||||
import ActiveLabel
|
||||
import MetaTextKit
|
||||
|
||||
final class ProfileFieldView: UIView {
|
||||
|
||||
|
@ -18,11 +18,7 @@ final class ProfileFieldView: UIView {
|
|||
let value = PassthroughSubject<String, Never>()
|
||||
|
||||
// for custom emoji display
|
||||
let titleActiveLabel: ActiveLabel = {
|
||||
let label = ActiveLabel(style: .profileFieldName)
|
||||
label.configure(content: "title", emojiDict: [:])
|
||||
return label
|
||||
}()
|
||||
let titleMetaLabel = MetaLabel(style: .profileFieldName)
|
||||
|
||||
// for editing
|
||||
let titleTextField: UITextField = {
|
||||
|
@ -34,12 +30,7 @@ final class ProfileFieldView: UIView {
|
|||
}()
|
||||
|
||||
// for custom emoji display
|
||||
let valueActiveLabel: ActiveLabel = {
|
||||
let label = ActiveLabel(style: .profileFieldValue)
|
||||
label.configure(content: "value", emojiDict: [:])
|
||||
label.textAlignment = .right
|
||||
return label
|
||||
}()
|
||||
let valueMetaLabel = MetaLabel(style: .profileFieldValue)
|
||||
|
||||
// for editing
|
||||
let valueTextField: UITextField = {
|
||||
|
@ -81,10 +72,10 @@ extension ProfileFieldView {
|
|||
containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
titleActiveLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerStackView.addArrangedSubview(titleActiveLabel)
|
||||
titleMetaLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerStackView.addArrangedSubview(titleMetaLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
titleActiveLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
||||
titleMetaLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
||||
])
|
||||
titleTextField.setContentHuggingPriority(.defaultLow - 1, for: .horizontal)
|
||||
titleTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -94,12 +85,12 @@ extension ProfileFieldView {
|
|||
])
|
||||
titleTextField.setContentHuggingPriority(.defaultLow - 1, for: .horizontal)
|
||||
|
||||
valueActiveLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerStackView.addArrangedSubview(valueActiveLabel)
|
||||
valueMetaLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerStackView.addArrangedSubview(valueMetaLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
valueActiveLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
||||
valueMetaLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
||||
])
|
||||
valueActiveLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
||||
valueMetaLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
||||
valueTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerStackView.addArrangedSubview(valueTextField)
|
||||
NSLayoutConstraint.activate([
|
||||
|
@ -137,7 +128,8 @@ struct ProfileFieldView_Previews: PreviewProvider {
|
|||
static var previews: some View {
|
||||
UIViewPreview(width: 375) {
|
||||
let filedView = ProfileFieldView()
|
||||
filedView.valueActiveLabel.configure(field: "https://mastodon.online", emojiDict: [:])
|
||||
let content = PlaintextMetaContent(string: "https://mastodon.online")
|
||||
filedView.valueMetaLabel.configure(content: content)
|
||||
return filedView
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 100))
|
||||
|
|
|
@ -8,16 +8,14 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import ActiveLabel
|
||||
import TwitterTextEditor
|
||||
import FLAnimatedImage
|
||||
import MetaTextView
|
||||
import MetaTextKit
|
||||
|
||||
protocol ProfileHeaderViewDelegate: AnyObject {
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarImageViewDidPressed imageView: UIImageView)
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, bannerImageViewDidPressed imageView: UIImageView)
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton)
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, activeLabel: ActiveLabel, entityDidPressed entity: ActiveEntity)
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, metaTextView: MetaTextView, metaDidPressed meta: Meta)
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView)
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed followingDashboardMeterView: ProfileStatusDashboardMeterView)
|
||||
|
@ -166,28 +164,38 @@ final class ProfileHeaderView: UIView {
|
|||
}()
|
||||
|
||||
let bioContainerView = UIView()
|
||||
let bioContainerStackView = UIStackView()
|
||||
let fieldContainerStackView = UIStackView()
|
||||
|
||||
let bioActiveLabelContainer: UIView = {
|
||||
// use to set margin for active label
|
||||
// the display/edit mode bio transition animation should without flicker with that
|
||||
let view = UIView()
|
||||
// note: comment out to see how it works
|
||||
view.layoutMargins = UIEdgeInsets(top: 8, left: 5, bottom: 8, right: 5) // magic from TextEditorView
|
||||
return view
|
||||
}()
|
||||
let bioActiveLabel = ActiveLabel(style: .default)
|
||||
let bioTextEditorView: TextEditorView = {
|
||||
let textEditorView = TextEditorView()
|
||||
textEditorView.scrollView.isScrollEnabled = false
|
||||
textEditorView.isScrollEnabled = false
|
||||
textEditorView.font = .preferredFont(forTextStyle: .body)
|
||||
textEditorView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color
|
||||
textEditorView.layer.masksToBounds = true
|
||||
textEditorView.layer.cornerCurve = .continuous
|
||||
textEditorView.layer.cornerRadius = 10
|
||||
return textEditorView
|
||||
|
||||
let bioMetaText: MetaText = {
|
||||
let metaText = MetaText()
|
||||
metaText.textView.backgroundColor = .clear
|
||||
metaText.textView.isEditable = false
|
||||
metaText.textView.isSelectable = true
|
||||
metaText.textView.isScrollEnabled = false
|
||||
//metaText.textView.textContainer.lineFragmentPadding = 0
|
||||
//metaText.textView.textContainerInset = .zero
|
||||
metaText.textView.layer.masksToBounds = false
|
||||
metaText.textView.textDragInteraction?.isEnabled = false // disable drag for link and attachment
|
||||
|
||||
metaText.textView.layer.masksToBounds = true
|
||||
metaText.textView.layer.cornerCurve = .continuous
|
||||
metaText.textView.layer.cornerRadius = 10
|
||||
|
||||
metaText.paragraphStyle = {
|
||||
let style = NSMutableParagraphStyle()
|
||||
style.lineSpacing = 5
|
||||
style.paragraphSpacing = 8
|
||||
return style
|
||||
}()
|
||||
metaText.textAttributes = [
|
||||
.font: UIFont.preferredFont(forTextStyle: .body),
|
||||
.foregroundColor: Asset.Colors.Label.primary.color,
|
||||
]
|
||||
metaText.linkAttributes = [
|
||||
.font: UIFont.preferredFont(forTextStyle: .body),
|
||||
.foregroundColor: Asset.Colors.brandBlue.color,
|
||||
]
|
||||
return metaText
|
||||
}()
|
||||
|
||||
static func createFieldCollectionViewLayout() -> UICollectionViewLayout {
|
||||
|
@ -405,28 +413,15 @@ extension ProfileHeaderView {
|
|||
bioContainerView.preservesSuperviewLayoutMargins = true
|
||||
metaContainerStackView.addArrangedSubview(bioContainerView)
|
||||
|
||||
bioContainerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
bioContainerView.addSubview(bioContainerStackView)
|
||||
bioMetaText.textView.translatesAutoresizingMaskIntoConstraints = false
|
||||
bioContainerView.addSubview(bioMetaText.textView)
|
||||
NSLayoutConstraint.activate([
|
||||
bioContainerStackView.topAnchor.constraint(equalTo: bioContainerView.topAnchor),
|
||||
bioContainerStackView.leadingAnchor.constraint(equalTo: bioContainerView.readableContentGuide.leadingAnchor),
|
||||
bioContainerStackView.trailingAnchor.constraint(equalTo: bioContainerView.readableContentGuide.trailingAnchor),
|
||||
bioContainerStackView.bottomAnchor.constraint(equalTo: bioContainerView.bottomAnchor),
|
||||
bioMetaText.textView.topAnchor.constraint(equalTo: bioContainerView.topAnchor),
|
||||
bioMetaText.textView.leadingAnchor.constraint(equalTo: bioContainerView.readableContentGuide.leadingAnchor),
|
||||
bioMetaText.textView.trailingAnchor.constraint(equalTo: bioContainerView.readableContentGuide.trailingAnchor),
|
||||
bioMetaText.textView.bottomAnchor.constraint(equalTo: bioContainerView.bottomAnchor),
|
||||
])
|
||||
|
||||
bioActiveLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
bioActiveLabelContainer.addSubview(bioActiveLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
bioActiveLabel.topAnchor.constraint(equalTo: bioActiveLabelContainer.layoutMarginsGuide.topAnchor),
|
||||
bioActiveLabel.leadingAnchor.constraint(equalTo: bioActiveLabelContainer.layoutMarginsGuide.leadingAnchor),
|
||||
bioActiveLabel.trailingAnchor.constraint(equalTo: bioActiveLabelContainer.layoutMarginsGuide.trailingAnchor),
|
||||
bioActiveLabel.bottomAnchor.constraint(equalTo: bioActiveLabelContainer.layoutMarginsGuide.bottomAnchor),
|
||||
])
|
||||
|
||||
bioContainerStackView.axis = .vertical
|
||||
bioContainerStackView.addArrangedSubview(bioActiveLabelContainer)
|
||||
bioContainerStackView.addArrangedSubview(bioTextEditorView)
|
||||
|
||||
fieldCollectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
metaContainerStackView.addArrangedSubview(fieldCollectionView)
|
||||
fieldCollectionViewHeightLayoutConstraint = fieldCollectionView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh)
|
||||
|
@ -445,7 +440,7 @@ extension ProfileHeaderView {
|
|||
bringSubviewToFront(bannerContainerView)
|
||||
bringSubviewToFront(nameContainerStackView)
|
||||
|
||||
bioActiveLabel.delegate = self
|
||||
bioMetaText.textView.linkDelegate = self
|
||||
|
||||
let avatarImageViewSingleTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||
avatarImageView.addGestureRecognizer(avatarImageViewSingleTapGestureRecognizer)
|
||||
|
@ -479,9 +474,8 @@ extension ProfileHeaderView {
|
|||
nameMetaText.textView.alpha = 1
|
||||
nameTextField.alpha = 0
|
||||
nameTextField.isEnabled = false
|
||||
bioActiveLabelContainer.isHidden = false
|
||||
bioTextEditorView.isHidden = true
|
||||
|
||||
bioMetaText.textView.backgroundColor = .clear
|
||||
|
||||
animator.addAnimations {
|
||||
self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor
|
||||
self.nameTextFieldBackgroundView.backgroundColor = .clear
|
||||
|
@ -494,17 +488,15 @@ extension ProfileHeaderView {
|
|||
nameMetaText.textView.alpha = 0
|
||||
nameTextField.isEnabled = true
|
||||
nameTextField.alpha = 1
|
||||
bioActiveLabelContainer.isHidden = true
|
||||
bioTextEditorView.isHidden = false
|
||||
|
||||
editAvatarBackgroundView.isHidden = false
|
||||
editAvatarBackgroundView.alpha = 0
|
||||
bioTextEditorView.backgroundColor = .clear
|
||||
bioMetaText.textView.backgroundColor = .clear
|
||||
animator.addAnimations {
|
||||
self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundEditingColor
|
||||
self.nameTextFieldBackgroundView.backgroundColor = Asset.Scene.Profile.Banner.nameEditBackgroundGray.color
|
||||
self.editAvatarBackgroundView.alpha = 1
|
||||
self.bioTextEditorView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color
|
||||
self.bioMetaText.textView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -530,11 +522,11 @@ extension ProfileHeaderView {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - ActiveLabelDelegate
|
||||
extension ProfileHeaderView: ActiveLabelDelegate {
|
||||
func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select entity: %s", ((#file as NSString).lastPathComponent), #line, #function, entity.primaryText)
|
||||
delegate?.profileHeaderView(self, activeLabel: activeLabel, entityDidPressed: entity)
|
||||
// MARK: - MetaTextViewDelegate
|
||||
extension ProfileHeaderView: MetaTextViewDelegate {
|
||||
func metaTextView(_ metaTextView: MetaTextView, didSelectMeta meta: Meta) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select entity", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
delegate?.profileHeaderView(self, metaTextView: metaTextView, metaDidPressed: meta)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import ActiveLabel
|
||||
import MastodonMeta
|
||||
import MetaTextKit
|
||||
|
||||
final class ProfileViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
|
||||
|
@ -316,20 +317,26 @@ extension ProfileViewController {
|
|||
// bind view model
|
||||
Publishers.CombineLatest3(
|
||||
viewModel.name,
|
||||
viewModel.emojiDict,
|
||||
viewModel.emojiMeta,
|
||||
viewModel.statusesCount
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] name, emojiDict, statusesCount in
|
||||
.sink { [weak self] name, emojiMeta, statusesCount in
|
||||
guard let self = self else { return }
|
||||
guard let title = name, let statusesCount = statusesCount,
|
||||
let formattedStatusCount = MastodonMetricFormatter().string(from: statusesCount) else {
|
||||
self.titleView.isHidden = true
|
||||
return
|
||||
}
|
||||
let subtitle = L10n.Plural.Count.MetricFormatted.post(formattedStatusCount, statusesCount)
|
||||
self.titleView.update(title: title, subtitle: subtitle, emojiDict: emojiDict)
|
||||
self.titleView.isHidden = false
|
||||
let subtitle = L10n.Plural.Count.MetricFormatted.post(formattedStatusCount, statusesCount)
|
||||
let mastodonContent = MastodonContent(content: title, emojis: emojiMeta)
|
||||
do {
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
self.titleView.update(titleMetaContent: metaContent, subtitle: subtitle)
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
viewModel.name
|
||||
|
@ -391,9 +398,9 @@ extension ProfileViewController {
|
|||
viewModel.accountForEdit
|
||||
.assign(to: \.value, on: profileHeaderViewController.viewModel.accountForEdit)
|
||||
.store(in: &disposeBag)
|
||||
viewModel.emojiDict
|
||||
viewModel.emojiMeta
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.value, on: profileHeaderViewController.viewModel.emojiDict)
|
||||
.assign(to: \.value, on: profileHeaderViewController.viewModel.emojiMeta)
|
||||
.store(in: &disposeBag)
|
||||
viewModel.username
|
||||
.map { username in username.flatMap { "@" + $0 } ?? " " }
|
||||
|
@ -695,7 +702,7 @@ extension ProfileViewController: UIScrollViewDelegate {
|
|||
|
||||
// MARK: - ProfileHeaderViewControllerDelegate
|
||||
extension ProfileViewController: ProfileHeaderViewControllerDelegate {
|
||||
|
||||
|
||||
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView) {
|
||||
guard let scrollView = (profileSegmentedViewController.pagingViewController.currentViewController as? UserTimelineViewController)?.scrollView else {
|
||||
// assertionFailure()
|
||||
|
@ -712,23 +719,24 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
|
|||
)
|
||||
}
|
||||
|
||||
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||
// handle profile fields interaction
|
||||
switch entity.type {
|
||||
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta) {
|
||||
switch meta {
|
||||
case .url(_, _, let url, _):
|
||||
guard let url = URL(string: url) else { return }
|
||||
coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
||||
case .hashtag(let hashtag, _):
|
||||
case .hashtag(_, let hashtag, _):
|
||||
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag)
|
||||
coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show)
|
||||
case .mention(_, let userInfo):
|
||||
case .mention(_, _, let userInfo):
|
||||
guard let href = userInfo?["href"] as? String else {
|
||||
// currently we cannot present profile scene without userID
|
||||
return
|
||||
}
|
||||
guard let url = URL(string: href) else { return }
|
||||
coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
||||
default:
|
||||
case .email:
|
||||
break
|
||||
case .emoji:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -758,7 +766,7 @@ extension ProfileViewController: ProfilePagingViewControllerDelegate {
|
|||
|
||||
// MARK: - ProfileHeaderViewDelegate
|
||||
extension ProfileViewController: ProfileHeaderViewDelegate {
|
||||
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarImageViewDidPressed imageView: UIImageView) {
|
||||
guard let mastodonUser = viewModel.mastodonUser.value else { return }
|
||||
guard let avatar = imageView.image else { return }
|
||||
|
@ -953,24 +961,24 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, activeLabel: ActiveLabel, entityDidPressed entity: ActiveEntity) {
|
||||
switch entity.type {
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, metaTextView: MetaTextView, metaDidPressed meta: Meta) {
|
||||
switch meta {
|
||||
case .url(_, _, let url, _):
|
||||
guard let url = URL(string: url) else { return }
|
||||
coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
||||
case .mention(_, let userInfo):
|
||||
case .mention(_, _, let userInfo):
|
||||
guard let href = userInfo?["href"] as? String,
|
||||
let url = URL(string: href) else { return }
|
||||
let url = URL(string: href) else { return }
|
||||
coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
||||
case .hashtag(let hashtag, _):
|
||||
case .hashtag(_, let hashtag, _):
|
||||
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag)
|
||||
coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show)
|
||||
default:
|
||||
case .email, .emoji:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
|||
import Combine
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
|
||||
// please override this base class
|
||||
class ProfileViewModel: NSObject {
|
||||
|
@ -40,7 +41,7 @@ class ProfileViewModel: NSObject {
|
|||
let followingCount: CurrentValueSubject<Int?, Never>
|
||||
let followersCount: CurrentValueSubject<Int?, Never>
|
||||
let fields: CurrentValueSubject<[Mastodon.Entity.Field], Never>
|
||||
let emojiDict: CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>
|
||||
let emojiMeta: CurrentValueSubject<MastodonContent.Emojis, Never>
|
||||
|
||||
// fulfill this before editing
|
||||
let accountForEdit = CurrentValueSubject<Mastodon.Entity.Account?, Never>(nil)
|
||||
|
@ -83,7 +84,7 @@ class ProfileViewModel: NSObject {
|
|||
self.protected = CurrentValueSubject(mastodonUser?.locked)
|
||||
self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false)
|
||||
self.fields = CurrentValueSubject(mastodonUser?.fields ?? [])
|
||||
self.emojiDict = CurrentValueSubject(mastodonUser?.emojiDict ?? [:])
|
||||
self.emojiMeta = CurrentValueSubject(mastodonUser?.emojiMeta ?? [:])
|
||||
super.init()
|
||||
|
||||
relationshipActionOptionSet
|
||||
|
@ -258,7 +259,7 @@ extension ProfileViewModel {
|
|||
self.protected.value = mastodonUser?.locked
|
||||
self.suspended.value = mastodonUser?.suspended ?? false
|
||||
self.fields.value = mastodonUser?.fields ?? []
|
||||
self.emojiDict.value = mastodonUser?.emojiDict ?? [:]
|
||||
self.emojiMeta.value = mastodonUser?.emojiMeta ?? [:]
|
||||
}
|
||||
|
||||
private func update(mastodonUser: MastodonUser?, currentMastodonUser: MastodonUser?) {
|
||||
|
|
|
@ -11,8 +11,8 @@ import CoreData
|
|||
import CoreDataStack
|
||||
import os.log
|
||||
import UIKit
|
||||
import TwitterTextEditor
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
|
||||
class ReportViewController: UIViewController, NeedsDependency {
|
||||
static let kAnimationDuration: TimeInterval = 0.33
|
||||
|
@ -92,6 +92,8 @@ class ReportViewController: UIViewController, NeedsDependency {
|
|||
}()
|
||||
|
||||
var bottomConstraint: NSLayoutConstraint!
|
||||
|
||||
let titleView = DoubleTitleLabelNavigationBarTitleView()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
@ -267,10 +269,19 @@ class ReportViewController: UIViewController, NeedsDependency {
|
|||
return nil
|
||||
}
|
||||
}()
|
||||
|
||||
navigationItem.title = L10n.Scene.Report.title(
|
||||
beReportedUser?.displayNameWithFallback ?? ""
|
||||
)
|
||||
|
||||
navigationItem.titleView = titleView
|
||||
if let user = beReportedUser {
|
||||
do {
|
||||
let mastodonConent = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonConent)
|
||||
titleView.update(titleMetaContent: metaContent, subtitle: nil)
|
||||
} catch {
|
||||
let metaContent = PlaintextMetaContent(string: user.displayNameWithFallback)
|
||||
titleView.update(titleMetaContent: metaContent, subtitle: nil)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func switchToStep2Content() {
|
||||
|
|
|
@ -11,9 +11,8 @@ import AVKit
|
|||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import ActiveLabel
|
||||
import Meta
|
||||
import MetaTextView
|
||||
import MetaTextKit
|
||||
|
||||
final class ReportedStatusTableViewCell: UITableViewCell, StatusCell {
|
||||
|
||||
|
@ -212,9 +211,6 @@ extension ReportedStatusTableViewCell: StatusViewDelegate {
|
|||
|
||||
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) {
|
||||
}
|
||||
|
||||
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||
}
|
||||
|
||||
func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@ import CoreDataStack
|
|||
import Foundation
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
import ActiveLabel
|
||||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
|
||||
protocol SearchRecommendAccountsCollectionViewCellDelegate: NSObject {
|
||||
func followButtonDidPressed(clickedUser: MastodonUser)
|
||||
|
@ -43,14 +44,7 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell {
|
|||
|
||||
let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
|
||||
|
||||
let displayNameLabel: ActiveLabel = {
|
||||
let label = ActiveLabel(style: .statusName)
|
||||
label.textColor = .white
|
||||
label.textAlignment = .center
|
||||
label.font = .systemFont(ofSize: 18, weight: .semibold)
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
let displayNameLabel = MetaLabel(style: .recommendAccountName)
|
||||
|
||||
let acctLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
|
@ -165,7 +159,14 @@ extension SearchRecommendAccountsCollectionViewCell {
|
|||
}
|
||||
|
||||
func config(with mastodonUser: MastodonUser) {
|
||||
displayNameLabel.configure(content: mastodonUser.displayNameWithFallback, emojiDict: mastodonUser.emojiDict)
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: mastodonUser.displayNameWithFallback, emojis: mastodonUser.emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
displayNameLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let metaContent = PlaintextMetaContent(string: mastodonUser.displayNameWithFallback)
|
||||
displayNameLabel.configure(content: metaContent)
|
||||
}
|
||||
acctLabel.text = "@" + mastodonUser.acct
|
||||
avatarImageView.af.setImage(
|
||||
withURL: URL(string: mastodonUser.avatar)!,
|
||||
|
|
|
@ -11,6 +11,8 @@ import Foundation
|
|||
import MastodonSDK
|
||||
import UIKit
|
||||
import FLAnimatedImage
|
||||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
|
||||
final class SearchResultTableViewCell: UITableViewCell {
|
||||
|
||||
|
@ -22,13 +24,7 @@ final class SearchResultTableViewCell: UITableViewCell {
|
|||
return imageView
|
||||
}()
|
||||
|
||||
let _titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.brandBlue.color
|
||||
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
return label
|
||||
}()
|
||||
let _titleLabel = MetaLabel(style: .statusName)
|
||||
|
||||
let _subTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
|
@ -155,13 +151,28 @@ extension SearchResultTableViewCell {
|
|||
|
||||
func config(with account: Mastodon.Entity.Account) {
|
||||
configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: account.avatarImageURL()))
|
||||
_titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName
|
||||
let name = account.displayName.isEmpty ? account.username : account.displayName
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: name, emojis: account.emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
_titleLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let metaContent = PlaintextMetaContent(string: name)
|
||||
_titleLabel.configure(content: metaContent)
|
||||
}
|
||||
_subTitleLabel.text = account.acct
|
||||
}
|
||||
|
||||
func config(with account: MastodonUser) {
|
||||
configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: account.avatarImageURL()))
|
||||
_titleLabel.text = account.displayNameWithFallback
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
_titleLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let metaContent = PlaintextMetaContent(string: account.displayNameWithFallback)
|
||||
_titleLabel.configure(content: metaContent)
|
||||
}
|
||||
_subTitleLabel.text = account.acct
|
||||
}
|
||||
|
||||
|
|
|
@ -8,10 +8,11 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import ActiveLabel
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
import AuthenticationServices
|
||||
|
||||
class SettingsViewController: UIViewController, NeedsDependency {
|
||||
|
@ -103,12 +104,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
|
|||
return tableView
|
||||
}()
|
||||
|
||||
let tableFooterActiveLabel: ActiveLabel = {
|
||||
let label = ActiveLabel(style: .default)
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
let tableFooterLabel = MetaLabel(style: .settingTableFooter)
|
||||
lazy var tableFooterView: UIView = {
|
||||
// init with a frame to fix a conflict ('UIView-Encapsulated-Layout-Height' UIStackView:0x7ffe41e47da0.height == 0)
|
||||
let view = UIStackView(frame: CGRect(x: 0, y: 0, width: 320, height: 320))
|
||||
|
@ -117,8 +113,8 @@ class SettingsViewController: UIViewController, NeedsDependency {
|
|||
view.axis = .vertical
|
||||
view.alignment = .center
|
||||
|
||||
tableFooterActiveLabel.delegate = self
|
||||
view.addArrangedSubview(tableFooterActiveLabel)
|
||||
tableFooterLabel.linkDelegate = self
|
||||
view.addArrangedSubview(tableFooterLabel)
|
||||
return view
|
||||
}()
|
||||
|
||||
|
@ -199,7 +195,15 @@ class SettingsViewController: UIViewController, NeedsDependency {
|
|||
let version = instance?.version ?? "-"
|
||||
let link = #"<a href="https://github.com/mastodon/mastodon">mastodon/mastodon</a>"#
|
||||
let content = L10n.Scene.Settings.Footer.mastodonDescription(link, version)
|
||||
self.tableFooterActiveLabel.configure(content: content, emojiDict: [:])
|
||||
let mastodonContent = MastodonContent(content: content, emojis: [:])
|
||||
do {
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
self.tableFooterLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let metaContent = PlaintextMetaContent(string: "")
|
||||
self.tableFooterLabel.configure(content: metaContent)
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
@ -365,6 +369,8 @@ extension SettingsViewController: UITableViewDelegate {
|
|||
// do nothing
|
||||
break
|
||||
case .boringZone(let link), .spicyZone(let link):
|
||||
let feedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
||||
feedbackGenerator.impactOccurred()
|
||||
switch link {
|
||||
case .accountSettings:
|
||||
guard let box = context.authenticationService.activeMastodonAuthenticationBox.value,
|
||||
|
@ -395,6 +401,7 @@ extension SettingsViewController: UITableViewDelegate {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
case .signOut:
|
||||
feedbackGenerator.impactOccurred()
|
||||
alertToSignout()
|
||||
}
|
||||
}
|
||||
|
@ -440,7 +447,8 @@ extension SettingsViewController: SettingsAppearanceTableViewCellDelegate {
|
|||
setting.update(appearanceRaw: appearanceMode.rawValue)
|
||||
}
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
let feedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
||||
feedbackGenerator.impactOccurred()
|
||||
}.store(in: &disposeBag)
|
||||
}
|
||||
}
|
||||
|
@ -482,6 +490,8 @@ extension SettingsViewController: SettingsToggleCellDelegate {
|
|||
setting.update(preferredTrueBlackDarkMode: isOn)
|
||||
case .disableAvatarAnimation:
|
||||
setting.update(preferredStaticAvatar: isOn)
|
||||
case .disableEmojiAnimation:
|
||||
setting.update(preferredStaticEmoji: isOn)
|
||||
case .useDefaultBrowser:
|
||||
setting.update(preferredUsingDefaultBrowser: isOn)
|
||||
}
|
||||
|
@ -494,6 +504,8 @@ extension SettingsViewController: SettingsToggleCellDelegate {
|
|||
ThemeService.shared.set(themeName: isOn ? .system : .mastodon)
|
||||
case .disableAvatarAnimation:
|
||||
UserDefaults.shared.preferredStaticAvatar = isOn
|
||||
case .disableEmojiAnimation:
|
||||
UserDefaults.shared.preferredStaticEmoji = isOn
|
||||
case .useDefaultBrowser:
|
||||
UserDefaults.shared.preferredUsingDefaultBrowser = isOn
|
||||
}
|
||||
|
@ -510,13 +522,16 @@ extension SettingsViewController: SettingsToggleCellDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
extension SettingsViewController: ActiveLabelDelegate {
|
||||
func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||
coordinator.present(
|
||||
scene: .safari(url: URL(string: "https://github.com/mastodon/mastodon")!),
|
||||
from: self,
|
||||
transition: .safariPresent(animated: true, completion: nil)
|
||||
)
|
||||
// MARK: - MetaLabelDelegate
|
||||
extension SettingsViewController: MetaLabelDelegate {
|
||||
func metaLabel(_ metaLabel: MetaLabel, didSelectMeta meta: Meta) {
|
||||
switch meta {
|
||||
case .url(_, _, let url, _):
|
||||
guard let url = URL(string: url) else { return }
|
||||
coordinator.present(scene: .safari(url: url), from: self, transition: .safariPresent(animated: true, completion: nil))
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -121,11 +121,9 @@ extension SettingsViewModel {
|
|||
|
||||
// preference
|
||||
snapshot.appendSections([.preference])
|
||||
let preferenceItems: [SettingsItem] = [
|
||||
.preference(settingObjectID: setting.objectID, preferenceType: .darkMode),
|
||||
.preference(settingObjectID: setting.objectID, preferenceType: .disableAvatarAnimation),
|
||||
.preference(settingObjectID: setting.objectID, preferenceType: .useDefaultBrowser),
|
||||
]
|
||||
let preferenceItems: [SettingsItem] = SettingsItem.PreferenceType.allCases.map { preferenceType in
|
||||
SettingsItem.preference(settingObjectID: setting.objectID, preferenceType: preferenceType)
|
||||
}
|
||||
snapshot.appendItems(preferenceItems,toSection: .preference)
|
||||
|
||||
// boring zone
|
||||
|
|
|
@ -6,19 +6,14 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import ActiveLabel
|
||||
import Meta
|
||||
import MetaTextKit
|
||||
|
||||
final class DoubleTitleLabelNavigationBarTitleView: UIView {
|
||||
|
||||
let containerView = UIStackView()
|
||||
|
||||
let titleLabel: ActiveLabel = {
|
||||
let label = ActiveLabel(style: .default)
|
||||
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
let titleLabel = MetaLabel(style: .titleView)
|
||||
|
||||
let subtitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
|
@ -58,9 +53,18 @@ extension DoubleTitleLabelNavigationBarTitleView {
|
|||
containerView.addArrangedSubview(titleLabel)
|
||||
containerView.addArrangedSubview(subtitleLabel)
|
||||
}
|
||||
|
||||
func update(title: String, subtitle: String?) {
|
||||
titleLabel.configure(content: PlaintextMetaContent(string: title))
|
||||
update(subtitle: subtitle)
|
||||
}
|
||||
|
||||
func update(title: String, subtitle: String?, emojiDict: MastodonStatusContent.EmojiDict) {
|
||||
titleLabel.configure(content: title, emojiDict: emojiDict)
|
||||
func update(titleMetaContent: MetaContent, subtitle: String?) {
|
||||
titleLabel.configure(content: titleMetaContent)
|
||||
update(subtitle: subtitle)
|
||||
}
|
||||
|
||||
func update(subtitle: String?) {
|
||||
if let subtitle = subtitle {
|
||||
subtitleLabel.text = subtitle
|
||||
subtitleLabel.isHidden = false
|
||||
|
|
|
@ -9,10 +9,9 @@ import os.log
|
|||
import UIKit
|
||||
import Combine
|
||||
import AVKit
|
||||
import ActiveLabel
|
||||
import AlamofireImage
|
||||
import FLAnimatedImage
|
||||
import MetaTextView
|
||||
import MetaTextKit
|
||||
import Meta
|
||||
import MastodonSDK
|
||||
|
||||
|
@ -26,7 +25,6 @@ protocol StatusViewDelegate: AnyObject {
|
|||
func statusView(_ statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||
func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
||||
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity)
|
||||
func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||
}
|
||||
|
||||
|
@ -81,12 +79,7 @@ final class StatusView: UIView {
|
|||
return label
|
||||
}()
|
||||
|
||||
let headerInfoLabel: ActiveLabel = {
|
||||
let label = ActiveLabel(style: .statusHeader)
|
||||
label.text = "Bob reblogged"
|
||||
label.layer.masksToBounds = false
|
||||
return label
|
||||
}()
|
||||
let headerInfoLabel = MetaLabel(style: .statusHeader)
|
||||
|
||||
let avatarView: UIView = {
|
||||
let view = UIView()
|
||||
|
@ -98,8 +91,8 @@ final class StatusView: UIView {
|
|||
let avatarButton = AvatarButton()
|
||||
let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton()
|
||||
|
||||
let nameLabel: ActiveLabel = {
|
||||
let label = ActiveLabel(style: .statusName)
|
||||
let nameMetaLabel: MetaLabel = {
|
||||
let label = MetaLabel(style: .statusName)
|
||||
return label
|
||||
}()
|
||||
|
||||
|
@ -220,7 +213,7 @@ final class StatusView: UIView {
|
|||
metaText.textView.layer.masksToBounds = false
|
||||
metaText.textView.textDragInteraction?.isEnabled = false // disable drag for link and attachment
|
||||
|
||||
let paragraphStyle: NSMutableParagraphStyle = {
|
||||
metaText.paragraphStyle = {
|
||||
let style = NSMutableParagraphStyle()
|
||||
style.lineSpacing = 5
|
||||
style.paragraphSpacing = 8
|
||||
|
@ -229,12 +222,10 @@ final class StatusView: UIView {
|
|||
metaText.textAttributes = [
|
||||
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)),
|
||||
.foregroundColor: Asset.Colors.Label.primary.color,
|
||||
.paragraphStyle: paragraphStyle,
|
||||
]
|
||||
metaText.linkAttributes = [
|
||||
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)),
|
||||
.foregroundColor: Asset.Colors.brandBlue.color,
|
||||
.paragraphStyle: paragraphStyle,
|
||||
]
|
||||
return metaText
|
||||
}()
|
||||
|
@ -296,6 +287,7 @@ extension StatusView {
|
|||
headerContainerStackView.trailingAnchor.constraint(equalTo: headerContainerView.trailingAnchor),
|
||||
headerContainerView.bottomAnchor.constraint(equalTo: headerContainerStackView.bottomAnchor, constant: StatusView.containerStackViewSpacing).priority(.defaultHigh),
|
||||
])
|
||||
headerContainerStackView.setContentCompressionResistancePriority(.required - 5, for: .vertical)
|
||||
containerStackView.addArrangedSubview(headerContainerView)
|
||||
defer {
|
||||
containerStackView.bringSubviewToFront(headerContainerView)
|
||||
|
@ -343,24 +335,27 @@ extension StatusView {
|
|||
titleContainerStackView.axis = .horizontal
|
||||
titleContainerStackView.alignment = .center
|
||||
titleContainerStackView.spacing = 4
|
||||
nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
titleContainerStackView.addArrangedSubview(nameLabel)
|
||||
nameMetaLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
titleContainerStackView.addArrangedSubview(nameMetaLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
nameLabel.heightAnchor.constraint(equalToConstant: 22).priority(.defaultHigh),
|
||||
nameMetaLabel.heightAnchor.constraint(equalToConstant: 22).priority(.defaultHigh),
|
||||
])
|
||||
titleContainerStackView.alignment = .firstBaseline
|
||||
titleContainerStackView.addArrangedSubview(nameTrialingDotLabel)
|
||||
titleContainerStackView.addArrangedSubview(dateLabel)
|
||||
titleContainerStackView.addArrangedSubview(UIView()) // padding
|
||||
let padding = UIView()
|
||||
padding.translatesAutoresizingMaskIntoConstraints = false
|
||||
titleContainerStackView.addArrangedSubview(padding) // padding
|
||||
titleContainerStackView.addArrangedSubview(visibilityImageView)
|
||||
nameLabel.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
|
||||
nameMetaLabel.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
|
||||
nameTrialingDotLabel.setContentHuggingPriority(.defaultHigh + 2, for: .horizontal)
|
||||
nameTrialingDotLabel.setContentCompressionResistancePriority(.required - 2, for: .horizontal)
|
||||
dateLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
dateLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||
visibilityImageView.setContentHuggingPriority(.required - 1, for: .horizontal)
|
||||
dateLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal)
|
||||
padding.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
padding.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
visibilityImageView.setContentHuggingPriority(.required - 9, for: .horizontal)
|
||||
visibilityImageView.setContentCompressionResistancePriority(.required - 9, for: .horizontal)
|
||||
visibilityImageView.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||
visibilityImageView.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||
|
||||
// subtitle container: [username]
|
||||
let subtitleContainerStackView = UIStackView()
|
||||
|
@ -424,17 +419,21 @@ extension StatusView {
|
|||
NSLayoutConstraint.activate([
|
||||
pollTableViewHeightLayoutConstraint,
|
||||
])
|
||||
|
||||
statusPollTableViewHeightObservation = pollTableView.observe(\.contentSize, options: .new, changeHandler: { [weak self] tableView, _ in
|
||||
guard let self = self else { return }
|
||||
guard self.pollTableView.contentSize.height != .zero else {
|
||||
self.pollTableViewHeightLayoutConstraint.constant = 44
|
||||
return
|
||||
}
|
||||
self.pollTableViewHeightLayoutConstraint.constant = self.pollTableView.contentSize.height
|
||||
})
|
||||
|
||||
|
||||
// statusPollTableViewHeightObservation = pollTableView.observe(\.contentSize, options: .new, changeHandler: { [weak self] tableView, _ in
|
||||
// guard let self = self else { return }
|
||||
// guard self.pollTableView.contentSize.height != .zero else {
|
||||
// self.pollTableViewHeightLayoutConstraint.constant = 44
|
||||
// return
|
||||
// }
|
||||
// self.pollTableViewHeightLayoutConstraint.constant = self.pollTableView.contentSize.height
|
||||
// })
|
||||
|
||||
pollStatusStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
statusContainerStackView.addArrangedSubview(pollStatusStackView)
|
||||
NSLayoutConstraint.activate([
|
||||
pollStatusStackView.heightAnchor.constraint(equalToConstant: 30).priority(.required - 10)
|
||||
])
|
||||
pollStatusStackView.axis = .horizontal
|
||||
pollStatusStackView.addArrangedSubview(pollVoteCountLabel)
|
||||
pollStatusStackView.addArrangedSubview(pollStatusDotLabel)
|
||||
|
@ -561,11 +560,10 @@ extension StatusView {
|
|||
|
||||
// MARK: - MetaTextViewDelegate
|
||||
extension StatusView: MetaTextViewDelegate {
|
||||
func metaTextView(_ metaTextView: MetaTextView, didSelectLink link: URL) {
|
||||
func metaTextView(_ metaTextView: MetaTextView, didSelectMeta meta: Meta) {
|
||||
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||
switch metaTextView {
|
||||
case contentMetaText.textView:
|
||||
guard let meta = Meta(url: link) else { return }
|
||||
delegate?.statusView(self, metaText: contentMetaText, didSelectMeta: meta)
|
||||
default:
|
||||
assertionFailure()
|
||||
|
@ -598,14 +596,6 @@ extension StatusView: UITextViewDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - ActiveLabelDelegate
|
||||
extension StatusView: ActiveLabelDelegate {
|
||||
func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select entity: %s", ((#file as NSString).lastPathComponent), #line, #function, entity.primaryText)
|
||||
delegate?.statusView(self, activeLabel: activeLabel, didSelectActiveEntity: entity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ContentWarningOverlayViewDelegate
|
||||
extension StatusView: ContentWarningOverlayViewDelegate {
|
||||
func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) {
|
||||
|
|
|
@ -11,11 +11,10 @@ import UIKit
|
|||
import Combine
|
||||
import AsyncDisplayKit
|
||||
import CoreDataStack
|
||||
import ActiveLabel
|
||||
import func AVFoundation.AVMakeRect
|
||||
|
||||
protocol StatusNodeDelegate: AnyObject {
|
||||
func statusNode(_ node: StatusNode, statusContentTextNode: ASMetaEditableTextNode, didSelectActiveEntityType type: ActiveEntityType)
|
||||
//func statusNode(_ node: StatusNode, statusContentTextNode: ASMetaEditableTextNode, didSelectActiveEntityType type: ActiveEntityType)
|
||||
}
|
||||
|
||||
final class StatusNode: ASCellNode {
|
||||
|
@ -29,21 +28,21 @@ final class StatusNode: ASCellNode {
|
|||
static let avatarImageSize = CGSize(width: 42, height: 42)
|
||||
static let avatarImageCornerRadius: CGFloat = 4
|
||||
|
||||
static let statusContentAppearance: MastodonStatusContent.Appearance = {
|
||||
let linkAttributes: [NSAttributedString.Key: Any] = [
|
||||
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)),
|
||||
.foregroundColor: Asset.Colors.brandBlue.color
|
||||
]
|
||||
return MastodonStatusContent.Appearance(
|
||||
attributes: [
|
||||
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)),
|
||||
.foregroundColor: Asset.Colors.Label.primary.color
|
||||
],
|
||||
urlAttributes: linkAttributes,
|
||||
hashtagAttributes: linkAttributes,
|
||||
mentionAttributes: linkAttributes
|
||||
)
|
||||
}()
|
||||
// static let statusContentAppearance: MastodonStatusContent.Appearance = {
|
||||
// let linkAttributes: [NSAttributedString.Key: Any] = [
|
||||
// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)),
|
||||
// .foregroundColor: Asset.Colors.brandBlue.color
|
||||
// ]
|
||||
// return MastodonStatusContent.Appearance(
|
||||
// attributes: [
|
||||
// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)),
|
||||
// .foregroundColor: Asset.Colors.Label.primary.color
|
||||
// ],
|
||||
// urlAttributes: linkAttributes,
|
||||
// hashtagAttributes: linkAttributes,
|
||||
// mentionAttributes: linkAttributes
|
||||
// )
|
||||
// }()
|
||||
|
||||
let avatarImageNode: ASNetworkImageNode = {
|
||||
let node = ASNetworkImageNode()
|
||||
|
@ -112,13 +111,14 @@ final class StatusNode: ASCellNode {
|
|||
.font: UIFont.systemFont(ofSize: 15, weight: .regular)
|
||||
])
|
||||
|
||||
statusContentTextNode.metaEditableTextNodeDelegate = self
|
||||
if let parseResult = try? MastodonStatusContent.parse(
|
||||
content: (status.reblog ?? status).content,
|
||||
emojiDict: (status.reblog ?? status).emojiDict
|
||||
) {
|
||||
statusContentTextNode.attributedText = parseResult.trimmedAttributedString(appearance: StatusNode.statusContentAppearance)
|
||||
}
|
||||
// FIXME:
|
||||
// statusContentTextNode.metaEditableTextNodeDelegate = self
|
||||
// if let parseResult = try? MastodonStatusContent.parse(
|
||||
// content: (status.reblog ?? status).content,
|
||||
// emojiDict: (status.reblog ?? status).emojiDict
|
||||
// ) {
|
||||
// statusContentTextNode.attributedText = parseResult.trimmedAttributedString(appearance: StatusNode.statusContentAppearance)
|
||||
// }
|
||||
|
||||
for imageNode in mediaMultiplexImageNodes {
|
||||
imageNode.dataSource = self
|
||||
|
@ -200,29 +200,19 @@ final class StatusNode: ASCellNode {
|
|||
|
||||
}
|
||||
|
||||
//extension StatusNode: ASImageDownloaderProtocol {
|
||||
// func downloadImage(with URL: URL, callbackQueue: DispatchQueue, downloadProgress: ASImageDownloaderProgress?, completion: @escaping ASImageDownloaderCompletion) -> Any? {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// func cancelImageDownload(forIdentifier downloadIdentifier: Any) {
|
||||
//
|
||||
// MARK: - ASEditableTextNodeDelegate
|
||||
//extension StatusNode: ASMetaEditableTextNodeDelegate {
|
||||
// func metaEditableTextNode(_ textNode: ASMetaEditableTextNode, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
||||
// guard let activityEntityType = ActiveEntityType(url: URL) else {
|
||||
// return false
|
||||
// }
|
||||
// defer {
|
||||
// delegate?.statusNode(self, statusContentTextNode: textNode, didSelectActiveEntityType: activityEntityType)
|
||||
// }
|
||||
// return false
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - ASEditableTextNodeDelegate
|
||||
extension StatusNode: ASMetaEditableTextNodeDelegate {
|
||||
func metaEditableTextNode(_ textNode: ASMetaEditableTextNode, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
||||
guard let activityEntityType = ActiveEntityType(url: URL) else {
|
||||
return false
|
||||
}
|
||||
defer {
|
||||
delegate?.statusNode(self, statusContentTextNode: textNode, didSelectActiveEntityType: activityEntityType)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ASMultiplexImageNodeDataSource
|
||||
extension StatusNode: ASMultiplexImageNodeDataSource {
|
||||
func multiplexImageNode(_ imageNode: ASMultiplexImageNode, urlForImageIdentifier imageIdentifier: ASImageIdentifier) -> URL? {
|
||||
|
|
|
@ -10,6 +10,8 @@ import Combine
|
|||
|
||||
final class PollOptionTableViewCell: UITableViewCell {
|
||||
|
||||
static let height: CGFloat = PollOptionView.height
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let pollOptionView = PollOptionView()
|
||||
|
|
|
@ -11,9 +11,8 @@ import AVKit
|
|||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import ActiveLabel
|
||||
import Meta
|
||||
import MetaTextView
|
||||
import MetaTextKit
|
||||
|
||||
protocol StatusTableViewCellDelegate: AnyObject {
|
||||
var context: AppContext! { get }
|
||||
|
@ -27,7 +26,6 @@ protocol StatusTableViewCellDelegate: AnyObject {
|
|||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||
|
@ -328,10 +326,6 @@ extension StatusTableViewCell: StatusViewDelegate {
|
|||
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) {
|
||||
delegate?.statusTableViewCell(self, statusView: statusView, pollVoteButtonPressed: button)
|
||||
}
|
||||
|
||||
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||
delegate?.statusTableViewCell(self, statusView: statusView, activeLabel: activeLabel, didSelectActiveEntity: entity)
|
||||
}
|
||||
|
||||
func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
|
||||
delegate?.statusTableViewCell(self, statusView: statusView, metaText: metaText, didSelectMeta: meta)
|
||||
|
|
|
@ -11,7 +11,8 @@ import CoreDataStack
|
|||
import Foundation
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
import ActiveLabel
|
||||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
|
||||
protocol SuggestionAccountTableViewCellDelegate: AnyObject {
|
||||
func accountButtonPressed(objectID: NSManagedObjectID, cell: SuggestionAccountTableViewCell)
|
||||
|
@ -29,13 +30,7 @@ final class SuggestionAccountTableViewCell: UITableViewCell {
|
|||
return imageView
|
||||
}()
|
||||
|
||||
let titleLabel: ActiveLabel = {
|
||||
let label = ActiveLabel(style: .statusName)
|
||||
label.textColor = Asset.Colors.brandBlue.color
|
||||
label.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
return label
|
||||
}()
|
||||
let titleLabel = MetaLabel(style: .statusName)
|
||||
|
||||
let subTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
|
@ -152,8 +147,15 @@ extension SuggestionAccountTableViewCell {
|
|||
imageTransition: .crossDissolve(0.2)
|
||||
)
|
||||
}
|
||||
titleLabel.configure(content: account.displayNameWithFallback, emojiDict: account.emojiDict)
|
||||
subTitleLabel.text = account.acct
|
||||
let mastodonContent = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojiMeta)
|
||||
do {
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
titleLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let metaContent = PlaintextMetaContent(string: account.displayNameWithFallback)
|
||||
titleLabel.configure(content: metaContent)
|
||||
}
|
||||
subTitleLabel.text = "@" + account.acct
|
||||
button.isSelected = isSelected
|
||||
button.publisher(for: .touchUpInside)
|
||||
.sink { [weak self] _ in
|
||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
|||
import Combine
|
||||
import CoreData
|
||||
import AVKit
|
||||
import MastodonMeta
|
||||
|
||||
final class ThreadViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
|
||||
|
@ -84,18 +85,27 @@ extension ThreadViewController {
|
|||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
viewModel.navigationBarTitle
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] tuple in
|
||||
guard let self = self else { return }
|
||||
guard let (title, emojiDict) = tuple else {
|
||||
self.titleView.update(title: L10n.Scene.Thread.backTitle, subtitle: nil, emojiDict: [:])
|
||||
return
|
||||
}
|
||||
self.titleView.update(title: title, subtitle: nil, emojiDict: emojiDict)
|
||||
|
||||
Publishers.CombineLatest(
|
||||
viewModel.navigationBarTitle,
|
||||
viewModel.navigationBarTitleEmojiMeta
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] title, emojiMeta in
|
||||
guard let self = self else { return }
|
||||
guard let title = title else {
|
||||
self.titleView.update(title: "", subtitle: nil)
|
||||
return
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
let mastodonContent = MastodonContent(content: title, emojis: emojiMeta ?? [:])
|
||||
do {
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
self.titleView.update(titleMetaContent: metaContent, subtitle: nil)
|
||||
} catch {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import CoreData
|
|||
import CoreDataStack
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
|
||||
class ThreadViewModel {
|
||||
|
||||
|
@ -45,7 +46,8 @@ class ThreadViewModel {
|
|||
let ancestorItems = CurrentValueSubject<[Item], Never>([])
|
||||
let descendantNodes = CurrentValueSubject<[LeafNode], Never>([])
|
||||
let descendantItems = CurrentValueSubject<[Item], Never>([])
|
||||
let navigationBarTitle: CurrentValueSubject<(String, MastodonStatusContent.EmojiDict)?, Never>
|
||||
let navigationBarTitle: CurrentValueSubject<String?, Never>
|
||||
let navigationBarTitleEmojiMeta: CurrentValueSubject<MastodonContent.Emojis, Never>
|
||||
|
||||
init(context: AppContext, optionalStatus: Status?) {
|
||||
self.context = context
|
||||
|
@ -53,8 +55,8 @@ class ThreadViewModel {
|
|||
self.rootItem = CurrentValueSubject(optionalStatus.flatMap { Item.root(statusObjectID: $0.objectID, attribute: Item.StatusAttribute()) })
|
||||
self.existStatusFetchedResultsController = StatusFetchedResultsController(managedObjectContext: context.managedObjectContext, domain: nil, additionalTweetPredicate: nil)
|
||||
self.navigationBarTitle = CurrentValueSubject(
|
||||
optionalStatus.flatMap { (L10n.Scene.Thread.title($0.author.displayNameWithFallback), $0.author.emojiDict) }
|
||||
)
|
||||
optionalStatus.flatMap { L10n.Scene.Thread.title($0.author.displayNameWithFallback) })
|
||||
self.navigationBarTitleEmojiMeta = CurrentValueSubject(optionalStatus.flatMap { $0.author.emojiMeta } ?? [:])
|
||||
|
||||
// bind fetcher domain
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
|
@ -85,7 +87,8 @@ class ThreadViewModel {
|
|||
return
|
||||
}
|
||||
self.rootNode.value = RootNode(domain: status.domain, statusID: status.id, replyToID: status.inReplyToID)
|
||||
self.navigationBarTitle.value = (L10n.Scene.Thread.title(status.author.displayNameWithFallback), status.author.emojiDict)
|
||||
self.navigationBarTitle.value = L10n.Scene.Thread.title(status.author.displayNameWithFallback)
|
||||
self.navigationBarTitleEmojiMeta.value = status.author.emojiMeta ?? [:]
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
|
|
@ -211,9 +211,15 @@ extension SettingService {
|
|||
UserDefaults.shared.preferredStaticAvatar = setting.preferredStaticAvatar
|
||||
}
|
||||
|
||||
// set emoji mode
|
||||
if UserDefaults.shared.preferredStaticEmoji != setting.preferredStaticEmoji {
|
||||
UserDefaults.shared.preferredStaticEmoji = setting.preferredStaticEmoji
|
||||
}
|
||||
|
||||
// set browser
|
||||
if UserDefaults.shared.preferredUsingDefaultBrowser != setting.preferredUsingDefaultBrowser {
|
||||
UserDefaults.shared.preferredUsingDefaultBrowser = setting.preferredUsingDefaultBrowser
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
//
|
||||
// StatusContentCacheService.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-17.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
final class StatusContentCacheService {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let cache = NSCache<Key, ParseResultWrapper>()
|
||||
|
||||
let workingQueue = DispatchQueue(label: "org.joinmastodon.app.BlurhashImageCacheService.working-queue", qos: .userInitiated, attributes: .concurrent)
|
||||
|
||||
func parseResult(content: String, emojiDict: MastodonStatusContent.EmojiDict) -> MastodonStatusContent.ParseResult? {
|
||||
let key = Key(content: content, emojiDict: emojiDict)
|
||||
return cache.object(forKey: key)?.parseResult
|
||||
}
|
||||
|
||||
func prefetch(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
||||
let key = Key(content: content, emojiDict: emojiDict)
|
||||
guard cache.object(forKey: key) == nil else { return }
|
||||
MastodonStatusContent.parseResult(content: content, emojiDict: emojiDict)
|
||||
.sink { [weak self] parseResult in
|
||||
guard let self = self else { return }
|
||||
guard let parseResult = parseResult else { return }
|
||||
let wrapper = ParseResultWrapper(parseResult: parseResult)
|
||||
self.cache.setObject(wrapper, forKey: key)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusContentCacheService {
|
||||
class Key: NSObject {
|
||||
let content: String
|
||||
let emojiDict: MastodonStatusContent.EmojiDict
|
||||
|
||||
init(content: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
||||
self.content = content
|
||||
self.emojiDict = emojiDict
|
||||
}
|
||||
|
||||
override func isEqual(_ object: Any?) -> Bool {
|
||||
guard let object = object as? Key else { return false }
|
||||
return object.content == content
|
||||
&& object.emojiDict == emojiDict
|
||||
}
|
||||
|
||||
override var hash: Int {
|
||||
return content.hashValue ^
|
||||
emojiDict.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
class ParseResultWrapper: NSObject {
|
||||
let parseResult: MastodonStatusContent.ParseResult
|
||||
|
||||
init(parseResult: MastodonStatusContent.ParseResult) {
|
||||
self.parseResult = parseResult
|
||||
}
|
||||
|
||||
override func isEqual(_ object: Any?) -> Bool {
|
||||
guard let object = object as? ParseResultWrapper else { return false }
|
||||
return object.parseResult == parseResult
|
||||
}
|
||||
|
||||
override var hash: Int {
|
||||
return parseResult.hashValue
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,7 +38,6 @@ class AppContext: ObservableObject {
|
|||
|
||||
let placeholderImageCacheService = PlaceholderImageCacheService()
|
||||
let blurhashImageCacheService = BlurhashImageCacheService()
|
||||
let statusContentCacheService = StatusContentCacheService()
|
||||
|
||||
let documentStore: DocumentStore
|
||||
private var documentStoreSubscription: AnyCancellable!
|
||||
|
|
Loading…
Reference in New Issue