Merge branch /develop into feature/in-reply-to-header

# Conflicts:
#	Mastodon.xcodeproj/project.pbxproj
#	Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift
#	Mastodon/State/AppContext.swift
#	MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift
#	MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift
This commit is contained in:
CMK 2021-03-17 11:24:25 +08:00
commit 6e87b581b9
90 changed files with 3127 additions and 156 deletions

View File

@ -14,6 +14,10 @@
"vote_failure": {
"title": "Vote Failure",
"poll_expired": "The poll has expired"
},
"discard_post_content": {
"title": "Discard Publish",
"message": "Confirm discard composed post content."
}
},
"controls": {
@ -27,6 +31,7 @@
"confirm": "Confirm",
"continue": "Continue",
"cancel": "Cancel",
"discard": "Discard",
"take_photo": "Take photo",
"save_photo": "Save photo",
"sign_in": "Sign In",
@ -72,17 +77,17 @@
},
"server_picker": {
"title": "Pick a Server,\nany server.",
"Button": {
"Category": {
"button": {
"category": {
"All": "All"
},
"SeeLess": "See Less",
"SeeMore": "See More"
"see_less": "See Less",
"see_more": "See More"
},
"Label": {
"Language": "LANGUAGE",
"Users": "USERS",
"Category": "CATEGORY"
"label": {
"language": "LANGUAGE",
"users": "USERS",
"category": "CATEGORY"
},
"input": {
"placeholder": "Find a server or join your own..."
@ -170,10 +175,24 @@
}
},
"home_timeline": {
"title": "Home"
"title": "Home",
"navigation_bar_state": {
"offline": "Offline",
"new_posts": "See new posts",
"published": "Published!",
"Publishing": "Publishing post..."
},
},
"public_timeline": {
"title": "Public"
},
"compose": {
"title": {
"new_post": "New Post",
"new_reply": "New Reply"
},
"content_input_placeholder": "Type or paste what's on your mind",
"compose_action": "Publish"
}
}
}
}

View File

@ -43,12 +43,13 @@
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 */; };
2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonContent.swift */; };
2D42FF6B25C817D2004A627A /* TootContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* TootContent.swift */; };
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 */; };
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */; };
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */; };
2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */; };
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */; };
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */; };
2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */ = {isa = PBXBuildFile; productRef = 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */; };
@ -72,6 +73,9 @@
2D7631B325C159F700929FB9 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631B225C159F700929FB9 /* Item.swift */; };
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */; };
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */; };
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarState.swift */; };
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift */; };
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D84350425FF858100EECE90 /* UIScrollView.swift */; };
2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0125C7E4F2004F19B8 /* Mention.swift */; };
2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0725C7E9A8004F19B8 /* Tag.swift */; };
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; };
@ -144,14 +148,24 @@
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; };
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */; };
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; };
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61325FF2C5600B98345 /* EmojiService.swift */; };
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */; };
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */; };
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */; };
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; };
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; };
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; };
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; };
DB55D33025FB630A0002F825 /* TwitterTextEditor+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB55D32F25FB630A0002F825 /* TwitterTextEditor+String.swift */; };
DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */; };
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */; };
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */; };
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F11725EFA35B001F1DAB /* StripProgressView.swift */; };
DB66728C25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */; };
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729525F9F91600D60309 /* ComposeStatusSection.swift */; };
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */; };
DB6672A325F9FDE500D60309 /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DB6672A225F9FDE500D60309 /* TwitterTextEditor */; };
DB6672A425F9FDE500D60309 /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6672A225F9FDE500D60309 /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; };
DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */; };
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
@ -165,6 +179,10 @@
DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */; };
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; };
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; };
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; };
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; };
DB789A1C25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1B25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift */; };
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift */; };
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; };
DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -202,8 +220,12 @@
DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */; };
DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2D25E504AC0051B173 /* Attachment.swift */; };
DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C3725E508BE0051B173 /* Attachment.swift */; };
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */; };
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; };
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */; };
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */; };
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; };
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; };
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; };
@ -254,6 +276,7 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
DB6672A425F9FDE500D60309 /* TwitterTextEditor in Embed Frameworks */,
DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
@ -296,12 +319,13 @@
2D38F1FD25CD481700561493 /* StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProvider.swift; sourceTree = "<group>"; };
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposeBagCollectable.swift; sourceTree = "<group>"; };
2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITapGestureRecognizer.swift; sourceTree = "<group>"; };
2D42FF6A25C817D2004A627A /* MastodonContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonContent.swift; sourceTree = "<group>"; };
2D42FF6A25C817D2004A627A /* TootContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TootContent.swift; sourceTree = "<group>"; };
2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolBarContainer.swift; sourceTree = "<group>"; };
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestExpandedButton.swift; sourceTree = "<group>"; };
2D42FF8E25C8228A004A627A /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = "<group>"; };
2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+State.swift"; sourceTree = "<group>"; };
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLayoutConstraint.swift; sourceTree = "<group>"; };
2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarProgressView.swift; sourceTree = "<group>"; };
2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewController.swift; sourceTree = "<group>"; };
2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewModel.swift; sourceTree = "<group>"; };
2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlContainableScrollViews.swift; sourceTree = "<group>"; };
@ -323,6 +347,9 @@
2D7631B225C159F700929FB9 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; };
2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewControllerAppearance.swift; sourceTree = "<group>"; };
2D82BA0425E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModelNavigationDelegateShim.swift; sourceTree = "<group>"; };
2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarState.swift; sourceTree = "<group>"; };
2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarView.swift; sourceTree = "<group>"; };
2D84350425FF858100EECE90 /* UIScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollView.swift; sourceTree = "<group>"; };
2D927F0125C7E4F2004F19B8 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = "<group>"; };
2D927F0725C7E9A8004F19B8 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
2D927F0D25C7E9C9004F19B8 /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = "<group>"; };
@ -407,13 +434,21 @@
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = "<group>"; };
DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContext.swift; sourceTree = "<group>"; };
DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlayerContainerView+MediaTypeIndicotorView.swift"; sourceTree = "<group>"; };
DB49A61325FF2C5600B98345 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = "<group>"; };
DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel.swift"; sourceTree = "<group>"; };
DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel+LoadState.swift"; sourceTree = "<group>"; };
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CustomEmoji.swift"; sourceTree = "<group>"; };
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = "<group>"; };
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = "<group>"; };
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = "<group>"; };
DB55D32F25FB630A0002F825 /* TwitterTextEditor+String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TwitterTextEditor+String.swift"; sourceTree = "<group>"; };
DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+UITableViewDelegate.swift"; sourceTree = "<group>"; };
DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCellHeightCacheableContainer.swift; sourceTree = "<group>"; };
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Poll.swift"; sourceTree = "<group>"; };
DB59F11725EFA35B001F1DAB /* StripProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripProgressView.swift; sourceTree = "<group>"; };
DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+Diffable.swift"; sourceTree = "<group>"; };
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusSection.swift; sourceTree = "<group>"; };
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusItem.swift; sourceTree = "<group>"; };
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSKeyValueObservation.swift; sourceTree = "<group>"; };
DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkContentStatusBarStyleNavigationController.swift; sourceTree = "<group>"; };
DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
@ -427,6 +462,10 @@
DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Status.swift"; sourceTree = "<group>"; };
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = "<group>"; };
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = "<group>"; };
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = "<group>"; };
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = "<group>"; };
DB789A1B25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTootContentTableViewCell.swift; sourceTree = "<group>"; };
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToTootContentTableViewCell.swift; sourceTree = "<group>"; };
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DB89B9F025C10FD0008580ED /* CoreDataStack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataStack.h; sourceTree = "<group>"; };
DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -466,8 +505,12 @@
DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageViewModel.swift; sourceTree = "<group>"; };
DB9D6C2D25E504AC0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
DB9D6C3725E508BE0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedEdgesButton.swift; sourceTree = "<group>"; };
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Reblog.swift"; sourceTree = "<group>"; };
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIInterpolatingMotionEffect.swift; sourceTree = "<group>"; };
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = "<group>"; };
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = "<group>"; };
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewModel.swift; sourceTree = "<group>"; };
@ -481,6 +524,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
DB6672A325F9FDE500D60309 /* TwitterTextEditor in Frameworks */,
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
@ -531,6 +575,7 @@
0FAA0FDD25E0B5700017CCDE /* Welcome */ = {
isa = PBXGroup;
children = (
DBABE3F125ECAC4E00879EE5 /* View */,
0FAA0FDE25E0B57E0017CCDE /* WelcomeViewController.swift */,
);
path = Welcome;
@ -597,6 +642,7 @@
children = (
2D152A8B25C295CC009AA50C /* StatusView.swift */,
2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */,
2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */,
);
path = Content;
sourceTree = "<group>";
@ -622,6 +668,8 @@
2D38F1EA25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift */,
2D38F1F025CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift */,
2D38F1F625CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift */,
2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarState.swift */,
2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift */,
);
path = HomeTimeline;
sourceTree = "<group>";
@ -653,6 +701,7 @@
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */,
DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */,
0FAA101125E105390017CCDE /* PrimaryActionButton.swift */,
DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */,
);
path = Button;
sourceTree = "<group>";
@ -671,6 +720,7 @@
children = (
2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */,
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */,
DB55D32F25FB630A0002F825 /* TwitterTextEditor+String.swift */,
);
path = Vender;
sourceTree = "<group>";
@ -685,6 +735,7 @@
2DA6054625F716A2006356F9 /* PlaybackState.swift */,
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */,
DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */,
DB49A61925FF327D00B98345 /* EmojiService */,
);
path = Service;
sourceTree = "<group>";
@ -743,6 +794,7 @@
DB4481C525EE2ADA00BEFB67 /* PollSection.swift */,
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
);
path = Section;
sourceTree = "<group>";
@ -790,6 +842,7 @@
DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */,
DB1E347725F519300079D7DF /* PickServerItem.swift */,
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */,
);
path = Item;
sourceTree = "<group>";
@ -919,6 +972,7 @@
2D61335525C1886800CAE157 /* Service */,
DB8AF55525C1379F002E6C99 /* Scene */,
DB8AF54125C13647002E6C99 /* Coordinator */,
DB9E0D6925EDFFE500CFDD76 /* Helper */,
DB8AF56225C138BC002E6C99 /* Extension */,
2D5A3D0125CF8640002347D6 /* Vender */,
DB5086CB25CC0DB400C2C187 /* Preference */,
@ -966,6 +1020,7 @@
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */,
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */,
);
path = APIService;
sourceTree = "<group>";
@ -980,6 +1035,16 @@
path = CoreData;
sourceTree = "<group>";
};
DB49A61925FF327D00B98345 /* EmojiService */ = {
isa = PBXGroup;
children = (
DB49A61325FF2C5600B98345 /* EmojiService.swift */,
DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */,
DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */,
);
path = EmojiService;
sourceTree = "<group>";
};
DB5086CB25CC0DB400C2C187 /* Preference */ = {
isa = PBXGroup;
children = (
@ -988,6 +1053,14 @@
path = Preference;
sourceTree = "<group>";
};
DB55D32225FB4D320002F825 /* View */ = {
isa = PBXGroup;
children = (
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */,
);
path = View;
sourceTree = "<group>";
};
DB68A03825E900CC00CFDF14 /* Share */ = {
isa = PBXGroup;
children = (
@ -1023,6 +1096,27 @@
path = ServerRules;
sourceTree = "<group>";
};
DB789A1025F9F29B0071ACA0 /* Compose */ = {
isa = PBXGroup;
children = (
DB55D32225FB4D320002F825 /* View */,
DB789A2125F9F76D0071ACA0 /* TableViewCell */,
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */,
DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */,
);
path = Compose;
sourceTree = "<group>";
};
DB789A2125F9F76D0071ACA0 /* TableViewCell */ = {
isa = PBXGroup;
children = (
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift */,
DB789A1B25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift */,
);
path = TableViewCell;
sourceTree = "<group>";
};
DB89B9EF25C10FD0008580ED /* CoreDataStack */ = {
isa = PBXGroup;
children = (
@ -1124,6 +1218,7 @@
DB9D6BEE25E4F5370051B173 /* Search */,
DB9D6BFD25E4F57B0051B173 /* Notification */,
DB9D6C0825E4F5A60051B173 /* Profile */,
DB789A1025F9F29B0071ACA0 /* Compose */,
);
path = Scene;
sourceTree = "<group>";
@ -1139,8 +1234,10 @@
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */,
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */,
2DF123A625C3B0210020F248 /* ActiveLabel.swift */,
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */,
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */,
2D42FF6A25C817D2004A627A /* MastodonContent.swift */,
2D939AB425EDD8A90076FA61 /* String.swift */,
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */,
DB68A06225E905E000CFDF14 /* UIApplication.swift */,
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */,
2D42FF8E25C8228A004A627A /* UIButton.swift */,
@ -1154,6 +1251,12 @@
2D206B9125F60EA700143C56 /* UIControl.swift */,
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */,
DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */,
2D46976325C2A71500CF4AA9 /* UIIamge.swift */,
2D32EAB925CB9B0500C9ED86 /* UIView.swift */,
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */,
2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */,
2D84350425FF858100EECE90 /* UIScrollView.swift */,
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */,
);
path = Extension;
sourceTree = "<group>";
@ -1232,6 +1335,22 @@
path = Control;
sourceTree = "<group>";
};
DB9E0D6925EDFFE500CFDD76 /* Helper */ = {
isa = PBXGroup;
children = (
2D42FF6A25C817D2004A627A /* TootContent.swift */,
);
path = Helper;
sourceTree = "<group>";
};
DBABE3F125ECAC4E00879EE5 /* View */ = {
isa = PBXGroup;
children = (
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */,
);
path = View;
sourceTree = "<group>";
};
DBE0821A25CD382900FD6BBD /* Register */ = {
isa = PBXGroup;
children = (
@ -1283,6 +1402,7 @@
DB5086B725CC0D6400C2C187 /* Kingfisher */,
2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */,
2D939AC725EE14620076FA61 /* CropViewController */,
DB6672A225F9FDE500D60309 /* TwitterTextEditor */,
);
productName = Mastodon;
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
@ -1412,6 +1532,7 @@
DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */,
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */,
2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */,
DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
);
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
projectDirPath = "";
@ -1602,16 +1723,19 @@
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */,
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */,
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */,
0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */,
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */,
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */,
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */,
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */,
2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */,
0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */,
DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */,
DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */,
@ -1619,12 +1743,17 @@
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarView.swift in Sources */,
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarState.swift in Sources */,
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */,
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */,
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */,
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */,
0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */,
DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */,
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */,
@ -1634,6 +1763,7 @@
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */,
DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */,
DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */,
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */,
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
@ -1644,18 +1774,23 @@
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */,
DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */,
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */,
DB55D33025FB630A0002F825 /* TwitterTextEditor+String.swift in Sources */,
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */,
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */,
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */,
DB66728C25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift in Sources */,
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */,
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */,
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */,
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */,
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */,
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,
@ -1677,6 +1812,7 @@
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */,
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
DB789A1C25F9F76A0071ACA0 /* ComposeTootContentTableViewCell.swift in Sources */,
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
@ -1718,6 +1854,7 @@
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */,
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */,
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */,
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */,
@ -1727,7 +1864,7 @@
2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */,
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */,
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */,
2D42FF6B25C817D2004A627A /* TootContent.swift in Sources */,
2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */,
DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */,
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
@ -1742,13 +1879,17 @@
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */,
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */,
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentTableViewCell.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -2335,6 +2476,14 @@
minimumVersion = 6.1.0;
};
};
DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/twitter/TwitterTextEditor.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@ -2377,6 +2526,11 @@
package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = Kingfisher;
};
DB6672A225F9FDE500D60309 /* TwitterTextEditor */ = {
isa = XCSwiftPackageProductDependency;
package = DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */;
productName = TwitterTextEditor;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */

View File

@ -7,7 +7,7 @@
<key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>16</integer>
<integer>10</integer>
</dict>
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
<dict>

View File

@ -99,6 +99,15 @@
"revision": "dad97167bf1be16aeecd109130900995dd01c515",
"version": "2.6.0"
}
},
{
"package": "TwitterTextEditor",
"repositoryURL": "https://github.com/twitter/TwitterTextEditor.git",
"state": {
"branch": null,
"revision": "8aa914134c5b6aa46e862de63f239ec0e3b52a91",
"version": "1.0.0"
}
}
]
},

View File

@ -47,6 +47,9 @@ extension SceneCoordinator {
case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel)
case mastodonResendEmail(viewModel: MastodonResendEmailViewModel)
// compose
case compose(viewModel: ComposeViewModel)
// misc
case alertController(alertController: UIAlertController)
@ -82,7 +85,7 @@ extension SceneCoordinator {
// Check user authentication status and show onboarding if needs
do {
let request = MastodonAuthentication.sortedFetchRequest
if try appContext.managedObjectContext.fetch(request).isEmpty {
if try appContext.managedObjectContext.count(for: request) == 0 {
DispatchQueue.main.async {
self.present(
scene: .welcome,
@ -190,6 +193,10 @@ private extension SceneCoordinator {
let _viewController = MastodonResendEmailViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .compose(let viewModel):
let _viewController = ComposeViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .alertController(let alertController):
if let popoverPresentationController = alertController.popoverPresentationController {
assert(

View File

@ -0,0 +1,39 @@
//
// ComposeStatusItem.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-11.
//
import Foundation
import Combine
import CoreData
enum ComposeStatusItem {
case replyTo(statusObjectID: NSManagedObjectID)
case input(replyToStatusObjectID: NSManagedObjectID?, attribute: ComposeStatusAttribute)
}
extension ComposeStatusItem: Hashable { }
extension ComposeStatusItem {
final class ComposeStatusAttribute: Equatable, Hashable {
private let id = UUID()
let avatarURL = CurrentValueSubject<URL?, Never>(nil)
let displayName = CurrentValueSubject<String?, Never>(nil)
let username = CurrentValueSubject<String?, Never>(nil)
let composeContent = CurrentValueSubject<String?, Never>(nil)
static func == (lhs: ComposeStatusAttribute, rhs: ComposeStatusAttribute) -> Bool {
return lhs.avatarURL.value == rhs.avatarURL.value &&
lhs.displayName.value == rhs.displayName.value &&
lhs.username.value == rhs.username.value &&
lhs.composeContent.value == rhs.composeContent.value
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
}

View File

@ -0,0 +1,97 @@
//
// ComposeStatusSection.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-11.
//
import UIKit
import Combine
import CoreData
import CoreDataStack
import TwitterTextEditor
enum ComposeStatusSection: Equatable, Hashable {
case repliedTo
case status
}
extension ComposeStatusSection {
enum ComposeKind {
case post
case reply(repliedToStatusObjectID: NSManagedObjectID)
}
}
extension ComposeStatusSection {
static func tableViewDiffableDataSource(
for tableView: UITableView,
dependency: NeedsDependency,
managedObjectContext: NSManagedObjectContext,
composeKind: ComposeKind,
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate
) -> UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem> {
UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>(tableView: tableView) { [weak textEditorViewTextAttributesDelegate] tableView, indexPath, item -> UITableViewCell? in
switch item {
case .replyTo(let repliedToStatusObjectID):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToTootContentTableViewCell.self), for: indexPath) as! ComposeRepliedToTootContentTableViewCell
// TODO:
return cell
case .input(let replyToTootObjectID, let attribute):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeTootContentTableViewCell.self), for: indexPath) as! ComposeTootContentTableViewCell
managedObjectContext.perform {
guard let replyToTootObjectID = replyToTootObjectID,
let replyTo = managedObjectContext.object(with: replyToTootObjectID) as? Toot else {
cell.statusView.headerContainerStackView.isHidden = true
return
}
cell.statusView.headerContainerStackView.isHidden = false
cell.statusView.headerInfoLabel.text = "[TODO] \(replyTo.author.displayName)"
}
ComposeStatusSection.configure(cell: cell, attribute: attribute)
cell.textEditorView.textAttributesDelegate = textEditorViewTextAttributesDelegate
// self size input cell
cell.composeContent
.receive(on: DispatchQueue.main)
.sink { text in
tableView.beginUpdates()
tableView.endUpdates()
}
.store(in: &cell.disposeBag)
return cell
}
}
}
}
extension ComposeStatusSection {
static func configure(
cell: ComposeTootContentTableViewCell,
attribute: ComposeStatusItem.ComposeStatusAttribute
) {
// set avatar
attribute.avatarURL
.receive(on: DispatchQueue.main)
.sink { avatarURL in
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: avatarURL))
}
.store(in: &cell.disposeBag)
// set display name and username
Publishers.CombineLatest(
attribute.displayName.eraseToAnyPublisher(),
attribute.username.eraseToAnyPublisher()
)
.receive(on: DispatchQueue.main)
.sink { displayName, username in
cell.statusView.nameLabel.text = displayName
cell.statusView.usernameLabel.text = username
}
.store(in: &cell.disposeBag)
// bind compose content
cell.composeContent
.map { $0 as String? }
.assign(to: \.value, on: attribute.composeContent)
.store(in: &cell.disposeBag)
}
}

View File

@ -406,7 +406,7 @@ extension StatusSection {
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow)
}
} else {
assertionFailure()
// assertionFailure()
cell.pollCountdownSubscription = nil
cell.statusView.pollCountdownLabel.text = "-"
}

View File

@ -0,0 +1,30 @@
//
// UIInterpolatingMotionEffect.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-2.
//
import UIKit
extension UIInterpolatingMotionEffect {
static func motionEffect(
minX: CGFloat,
maxX: CGFloat,
minY: CGFloat,
maxY: CGFloat
) -> UIMotionEffectGroup {
let motionEffectX = UIInterpolatingMotionEffect(keyPath: "layer.transform.translation.x", type: .tiltAlongHorizontalAxis)
motionEffectX.minimumRelativeValue = minX
motionEffectX.maximumRelativeValue = maxX
let motionEffectY = UIInterpolatingMotionEffect(keyPath: "layer.transform.translation.y", type: .tiltAlongVerticalAxis)
motionEffectY.minimumRelativeValue = minY
motionEffectY.maximumRelativeValue = maxY
let motionEffectGroup = UIMotionEffectGroup()
motionEffectGroup.motionEffects = [motionEffectX, motionEffectY]
return motionEffectGroup
}
}

View File

@ -0,0 +1,32 @@
//
// UIScrollView.swift
// Mastodon
//
// Created by sxiaojian on 2021/3/15.
//
import UIKit
extension UIScrollView {
public enum ScrollDirection {
case top
case bottom
case left
case right
}
public func scroll(to direction: ScrollDirection, animated: Bool) {
let offset: CGPoint
switch direction {
case .top:
offset = CGPoint(x: contentOffset.x, y: -adjustedContentInset.top)
case .bottom:
offset = CGPoint(x: contentOffset.x, y: max(-adjustedContentInset.top, contentSize.height - frame.height + adjustedContentInset.bottom))
case .left:
offset = CGPoint(x: -adjustedContentInset.left, y: contentOffset.y)
case .right:
offset = CGPoint(x: max(-adjustedContentInset.left, contentSize.width - frame.width + adjustedContentInset.right), y: contentOffset.y)
}
setContentOffset(offset, animated: animated)
}
}

View File

@ -49,6 +49,7 @@ internal enum Asset {
internal static let actionToolbar = ColorAsset(name: "Colors/Button/action.toolbar")
internal static let disabled = ColorAsset(name: "Colors/Button/disabled")
internal static let highlight = ColorAsset(name: "Colors/Button/highlight")
internal static let normal = ColorAsset(name: "Colors/Button/normal")
}
internal enum Icon {
internal static let photo = ColorAsset(name: "Colors/Icon/photo")
@ -67,6 +68,10 @@ internal enum Asset {
internal static let invalid = ColorAsset(name: "Colors/TextField/invalid")
internal static let valid = ColorAsset(name: "Colors/TextField/valid")
}
internal static let backgroundLight = ColorAsset(name: "Colors/backgroundLight")
internal static let buttonDefault = ColorAsset(name: "Colors/buttonDefault")
internal static let buttonDisabled = ColorAsset(name: "Colors/buttonDisabled")
internal static let buttonInactive = ColorAsset(name: "Colors/buttonInactive")
internal static let lightAlertYellow = ColorAsset(name: "Colors/lightAlertYellow")
internal static let lightBackground = ColorAsset(name: "Colors/lightBackground")
internal static let lightBrandBlue = ColorAsset(name: "Colors/lightBrandBlue")
@ -82,6 +87,16 @@ internal enum Asset {
internal static let systemOrange = ColorAsset(name: "Colors/system.orange")
}
internal enum Welcome {
internal enum Illustration {
internal static let backgroundCyan = ColorAsset(name: "Welcome/illustration/background.cyan")
internal static let cloudBase = ImageAsset(name: "Welcome/illustration/cloud.base")
internal static let elephantOnAirplaneWithContrail = ImageAsset(name: "Welcome/illustration/elephant.on.airplane.with.contrail")
internal static let elephantThreeOnGrass = ImageAsset(name: "Welcome/illustration/elephant.three.on.grass")
internal static let elephantThreeOnGrassWithTreeThree = ImageAsset(name: "Welcome/illustration/elephant.three.on.grass.with.tree.three")
internal static let elephantThreeOnGrassWithTreeTwo = ImageAsset(name: "Welcome/illustration/elephant.three.on.grass.with.tree.two")
}
internal static let mastodonLogoBlack = ImageAsset(name: "Welcome/mastodon.logo.black")
internal static let mastodonLogoBlackLarge = ImageAsset(name: "Welcome/mastodon.logo.black.large")
internal static let mastodonLogo = ImageAsset(name: "Welcome/mastodon.logo")
internal static let mastodonLogoLarge = ImageAsset(name: "Welcome/mastodon.logo.large")
}

View File

@ -19,6 +19,12 @@ internal enum L10n {
/// Please try again later.
internal static let pleaseTryAgainLater = L10n.tr("Localizable", "Common.Alerts.Common.PleaseTryAgainLater")
}
internal enum DiscardPostContent {
/// Confirm discard composed post content.
internal static let message = L10n.tr("Localizable", "Common.Alerts.DiscardPostContent.Message")
/// Discard Publish
internal static let title = L10n.tr("Localizable", "Common.Alerts.DiscardPostContent.Title")
}
internal enum ServerError {
/// Server Error
internal static let title = L10n.tr("Localizable", "Common.Alerts.ServerError.Title")
@ -46,6 +52,8 @@ internal enum L10n {
internal static let confirm = L10n.tr("Localizable", "Common.Controls.Actions.Confirm")
/// Continue
internal static let `continue` = L10n.tr("Localizable", "Common.Controls.Actions.Continue")
/// Discard
internal static let discard = L10n.tr("Localizable", "Common.Controls.Actions.Discard")
/// Edit
internal static let edit = L10n.tr("Localizable", "Common.Controls.Actions.Edit")
/// OK
@ -131,6 +139,18 @@ internal enum L10n {
}
internal enum Scene {
internal enum Compose {
/// Publish
internal static let composeAction = L10n.tr("Localizable", "Scene.Compose.ComposeAction")
/// Type or paste what's on your mind
internal static let contentInputPlaceholder = L10n.tr("Localizable", "Scene.Compose.ContentInputPlaceholder")
internal enum Title {
/// New Post
internal static let newPost = L10n.tr("Localizable", "Scene.Compose.Title.NewPost")
/// New Reply
internal static let newReply = L10n.tr("Localizable", "Scene.Compose.Title.NewReply")
}
}
internal enum ConfirmEmail {
/// We just sent an email to %@,\ntap the link to confirm your account.
internal static func subtitle(_ p1: Any) -> String {
@ -166,6 +186,16 @@ internal enum L10n {
internal enum HomeTimeline {
/// Home
internal static let title = L10n.tr("Localizable", "Scene.HomeTimeline.Title")
internal enum NavigationBarState {
/// See new posts
internal static let newPosts = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.NewPosts")
/// Offline
internal static let offline = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Offline")
/// Published!
internal static let published = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Published")
/// Publishing post...
internal static let publishing = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Publishing")
}
}
internal enum PublicTimeline {
/// Public
@ -274,9 +304,9 @@ internal enum L10n {
internal static let title = L10n.tr("Localizable", "Scene.ServerPicker.Title")
internal enum Button {
/// See Less
internal static let seeless = L10n.tr("Localizable", "Scene.ServerPicker.Button.Seeless")
internal static let seeLess = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeLess")
/// See More
internal static let seemore = L10n.tr("Localizable", "Scene.ServerPicker.Button.Seemore")
internal static let seeMore = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeMore")
internal enum Category {
/// All
internal static let all = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.All")

View File

@ -1,5 +1,5 @@
//
// MastodonContent.swift
// TootContent.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021/2/1.

View File

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.784",
"green" : "0.682",
"red" : "0.608"
"blue" : "140",
"green" : "130",
"red" : "110"
}
},
"idiom" : "universal"

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "217",
"green" : "144",
"red" : "43"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.910",
"green" : "0.882",
"red" : "0.851"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.851",
"green" : "0.565",
"red" : "0.169"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.784",
"green" : "0.682",
"red" : "0.608"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.549",
"green" : "0.510",
"red" : "0.431"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,20 +1,20 @@
{
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"red" : "0.792",
"blue" : "0.016",
"green" : "0.561",
"red" : "0.792"
"alpha" : "1.000"
}
},
"idiom" : "universal"
}
}
],
"info" : {
"author" : "xcode",
"version" : 1
"version" : 1,
"author" : "xcode"
}
}
}

View File

@ -1,20 +1,20 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"red" : "0.875",
"blue" : "0.353",
"green" : "0.251",
"red" : "0.875"
"green" : "0.251"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
]
}

View File

@ -1,6 +1,11 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
@ -9,12 +14,7 @@
"green" : "0.137",
"red" : "0.122"
}
},
"idiom" : "universal"
}
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
]
}

View File

@ -1,20 +1,20 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"blue" : "0.263",
"green" : "0.235",
"alpha" : "0.600",
"blue" : "67",
"green" : "60",
"red" : "60"
}
},
"idiom" : "universal"
"red" : "0.235"
},
"color-space" : "srgb"
}
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
]
}

View File

@ -1,20 +1,20 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.604",
"green" : "0.741",
"red" : "0.475"
"red" : "0.475",
"blue" : "0.604"
}
},
"idiom" : "universal"
}
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
]
}

View File

@ -1,20 +1,20 @@
{
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.996",
"alpha" : "1.000",
"blue" : "0.996",
"green" : "1.000",
"red" : "0.996"
}
},
"idiom" : "universal"
"green" : "1.000"
},
"color-space" : "srgb"
}
}
],
"info" : {
"author" : "xcode",
"version" : 1
"version" : 1,
"author" : "xcode"
}
}
}

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "232",
"green" : "207",
"red" : "60"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "untitled10007Group61.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "untitled10007Group61@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "untitled10007Group61@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "untitled10006Group21.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "untitled10006Group21@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "untitled10006Group21@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "untitled10003Group11.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "untitled10003Group11@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "untitled10003Group11@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "untitled10005Group101.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "untitled10005Group101@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "untitled10005Group101@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "untitled10004Group111.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "untitled10004Group111@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "untitled10004Group111@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "mastodon.logo.black.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "mastodon.logo.black.large.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "mastodon.logo.pdf",
"filename" : "logotypeFull1.pdf",
"idiom" : "universal"
}
],

View File

@ -0,0 +1,513 @@
%PDF-1.7
1 0 obj
<< /BBox [ 0.000000 0.000000 261.000000 67.000000 ]
/Resources << >>
/Subtype /Form
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
>>
/Type /XObject
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 2.000000 2.000000 cm
0.000000 0.000000 0.000000 scn
0.000000 63.000000 m
257.000000 63.000000 l
257.000000 0.000000 l
0.000000 0.000000 l
0.000000 63.000000 l
h
f
n
Q
endstream
endobj
2 0 obj
234
endobj
3 0 obj
<< /BBox [ 0.000000 0.000000 261.000000 67.000000 ]
/Resources << >>
/Subtype /Form
/Length 4 0 R
/Group << /Type /Group
/S /Transparency
>>
/Type /XObject
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 2.000000 1.844727 cm
0.168627 0.564706 0.850980 scn
57.842663 25.387310 m
56.973518 20.943096 50.061783 16.079765 42.122841 15.137188 c
37.982918 14.645527 33.907391 14.194614 29.561213 14.392532 c
22.453136 14.716278 16.844669 16.079762 16.844669 16.079762 c
16.844669 15.391525 16.887453 14.736427 16.972567 14.123863 c
17.896652 7.149250 23.928431 6.731026 29.641823 6.536243 c
35.408352 6.340115 40.542614 7.950329 40.542614 7.950329 c
40.779942 2.765934 l
40.779942 2.765934 36.746300 0.612568 29.561213 0.216728 c
25.598721 0.000004 20.679268 0.315689 14.948763 1.823364 c
2.521337 5.094391 0.384049 18.266270 0.057106 31.631596 c
-0.042868 35.599815 0.018828 39.341911 0.018828 42.470993 c
0.018828 56.138123 9.024163 60.143955 9.024163 60.143955 c
13.564885 62.217625 21.356571 63.089451 29.456736 63.155273 c
29.655783 63.155273 l
37.755947 63.089451 45.552582 62.217625 50.093304 60.143955 c
50.093304 60.143955 59.098644 56.138123 59.098644 42.470993 c
59.098644 42.470993 59.211227 32.387894 57.842663 25.387310 c
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 14.485992 26.452484 cm
0.996078 1.000000 0.996078 scn
0.000000 21.175804 m
0.000000 23.165289 1.622104 24.777744 3.622489 24.777744 c
5.623325 24.777744 7.244979 23.165289 7.244979 21.175804 c
7.244979 19.186768 5.623325 17.573866 3.622489 17.573866 c
1.622104 17.573866 0.000000 19.186768 0.000000 21.175804 c
h
51.953178 16.803417 m
51.953178 0.255280 l
45.359833 0.255280 l
45.359833 16.317129 l
45.359833 19.703238 43.926872 21.421368 41.060944 21.421368 c
37.892841 21.421368 36.304512 19.382627 36.304512 15.351715 c
36.304512 6.560461 l
29.749895 6.560461 l
29.749895 15.351715 l
29.749895 19.382627 28.161566 21.421368 24.993464 21.421368 c
22.127537 21.421368 20.694574 19.703238 20.694574 16.317129 c
20.694574 0.255280 l
14.101229 0.255280 l
14.101229 16.803417 l
14.101229 20.185495 14.967223 22.873068 16.706865 24.861656 c
18.500998 26.849798 20.850389 27.868944 23.766304 27.868944 c
27.140659 27.868944 29.695858 26.579786 31.384611 24.000576 c
33.027431 21.262854 l
34.669796 24.000576 l
36.359001 26.579786 38.913750 27.868944 42.288555 27.868944 c
45.204472 27.868944 47.553417 26.849798 49.347549 24.861656 c
51.087193 22.873068 51.953178 20.185495 51.953178 16.803417 c
51.953178 16.803417 l
h
74.667320 8.577215 m
76.027779 10.006529 76.683022 11.806602 76.683022 13.977438 c
76.683022 16.148273 76.027779 17.948345 74.667320 19.324818 c
73.357300 20.754578 71.693764 21.442368 69.678070 21.442368 c
67.661919 21.442368 65.998833 20.754578 64.688812 19.324818 c
63.378338 17.948345 62.723106 16.148273 62.723106 13.977438 c
62.723106 11.806602 63.378338 10.006529 64.688812 8.577215 c
65.998833 7.200743 67.661919 6.512505 69.678070 6.512505 c
71.693764 6.512505 73.357300 7.200743 74.667320 8.577215 c
h
76.683022 27.213799 m
83.184502 27.213799 l
83.184502 0.741074 l
76.683022 0.741074 l
76.683022 3.865234 l
74.717758 1.270798 71.995941 0.000000 68.468475 0.000000 c
65.091866 0.000000 62.219185 1.323635 59.799988 4.024193 c
57.431683 6.724304 56.222076 10.059816 56.222076 13.977438 c
56.222076 17.842222 57.431683 21.178179 59.799988 23.878288 c
62.219185 26.578400 65.091866 27.954872 68.468475 27.954872 c
71.995941 27.954872 74.717758 26.684074 76.683022 24.090088 c
76.683022 27.213799 l
76.683022 27.213799 l
h
105.058311 14.453876 m
106.973137 13.024565 107.931007 11.012690 107.880569 8.471540 c
107.880569 5.770981 106.922699 3.653431 104.957443 2.170834 c
102.991730 0.741074 100.623421 0.000000 97.750740 0.000000 c
92.559738 0.000000 89.031815 2.118000 87.166977 6.300707 c
92.811928 9.635767 l
93.567589 7.359705 95.230667 6.194584 97.750740 6.194584 c
100.068611 6.194584 101.228233 6.936104 101.228233 8.471540 c
101.228233 9.583378 99.716003 10.589090 96.642021 11.383003 c
95.482407 11.700928 94.524994 12.018402 93.769333 12.283487 c
92.711052 12.706638 91.804077 13.183523 91.047966 13.765636 c
89.183136 15.194948 88.225716 17.101593 88.225716 19.536619 c
88.225716 22.131054 89.132698 24.195765 90.947098 25.678362 c
92.811928 27.213799 95.079361 27.954872 97.801178 27.954872 c
102.135201 27.954872 105.310501 26.101961 107.376183 22.342852 c
101.833023 19.166304 l
101.026474 20.965929 99.666016 21.865967 97.801178 21.865967 c
95.835472 21.865967 94.878067 21.124891 94.878067 19.695580 c
94.878067 18.583744 96.389832 17.578032 99.464264 16.783670 c
101.833023 16.254395 103.697403 15.460037 105.058311 14.453876 c
105.058311 14.453876 l
h
125.722229 20.648455 m
120.027290 20.648455 l
120.027290 9.635767 l
120.027290 8.312132 120.531677 7.518219 121.489082 7.147905 c
122.194763 6.882818 123.605659 6.829983 125.722229 6.936106 c
125.722229 0.741074 l
121.338226 0.211800 118.162918 0.635399 116.298080 2.065159 c
114.433701 3.441635 113.526268 5.982782 113.526268 9.635767 c
113.526268 20.648455 l
109.141808 20.648455 l
109.141808 27.213799 l
113.526268 27.213799 l
113.526268 32.561180 l
120.027290 34.625893 l
120.027290 27.213799 l
125.722229 27.213799 l
125.722229 20.648455 l
125.722229 20.648455 l
h
146.436966 8.735956 m
147.747437 10.112877 148.402237 11.860113 148.402237 13.977663 c
148.402237 16.095211 147.747437 17.842445 146.436966 19.218920 c
145.126495 20.595840 143.513840 21.283630 141.548141 21.283630 c
139.582886 21.283630 137.970245 20.595840 136.659760 19.218920 c
135.399734 17.789608 134.744492 16.042374 134.744492 13.977663 c
134.744492 11.912504 135.399734 10.165268 136.659760 8.735956 c
137.970245 7.359482 139.582886 6.671242 141.548141 6.671242 c
143.513840 6.671242 145.126495 7.359482 146.436966 8.735956 c
146.436966 8.735956 l
h
132.073563 4.023972 m
129.503494 6.724081 128.243469 10.006754 128.243469 13.977663 c
128.243469 17.895733 129.503494 21.177954 132.073563 23.878063 c
134.643616 26.578175 137.818924 27.954649 141.548141 27.954649 c
145.277802 27.954649 148.452667 26.578175 151.023178 23.878063 c
153.593689 21.177954 154.903702 17.842447 154.903702 13.977663 c
154.903702 10.059591 153.593689 6.724081 151.023178 4.023972 c
148.452667 1.323414 145.328247 0.000225 141.548141 0.000225 c
137.768478 0.000225 134.643616 1.323414 132.073563 4.023972 c
h
176.626083 8.577215 m
177.936554 10.006529 178.591339 11.806602 178.591339 13.977438 c
178.591339 16.148273 177.936554 17.948345 176.626083 19.324818 c
175.316055 20.754578 173.652527 21.442368 171.636826 21.442368 c
169.620682 21.442368 167.957596 20.754578 166.597137 19.324818 c
165.287109 17.948345 164.631424 16.148273 164.631424 13.977438 c
164.631424 11.806602 165.287109 10.006529 166.597137 8.577215 c
167.957596 7.200743 169.671112 6.512505 171.636826 6.512505 c
173.652527 6.512505 175.316055 7.200743 176.626083 8.577215 c
h
178.591339 37.802887 m
185.092789 37.802887 l
185.092789 0.741074 l
178.591339 0.741074 l
178.591339 3.865234 l
176.676529 1.270798 173.954697 0.000000 170.427216 0.000000 c
167.050613 0.000000 164.127945 1.323635 161.708755 4.024193 c
159.339996 6.724304 158.130402 10.059816 158.130402 13.977438 c
158.130402 17.842222 159.339996 21.178179 161.708755 23.878288 c
164.127945 26.578400 167.050613 27.954872 170.427216 27.954872 c
173.954697 27.954872 176.676529 26.684074 178.591339 24.090088 c
178.591339 37.802887 l
178.591339 37.802887 l
h
207.924301 8.735956 m
209.234329 10.112877 209.889572 11.860113 209.889572 13.977663 c
209.889572 16.095211 209.234329 17.842445 207.924301 19.218920 c
206.613831 20.595840 205.001190 21.283630 203.035477 21.283630 c
201.070221 21.283630 199.457123 20.595840 198.147110 19.218920 c
196.886612 17.789608 196.231812 16.042374 196.231812 13.977663 c
196.231812 11.912504 196.886612 10.165268 198.147110 8.735956 c
199.457123 7.359482 201.070221 6.671242 203.035477 6.671242 c
205.001190 6.671242 206.613831 7.359482 207.924301 8.735956 c
207.924301 8.735956 l
h
193.560883 4.023972 m
190.990372 6.724081 189.730804 10.006754 189.730804 13.977663 c
189.730804 17.895733 190.990372 21.177954 193.560883 23.878063 c
196.131393 26.578175 199.306259 27.954649 203.035477 27.954649 c
206.765137 27.954649 209.940002 26.578175 212.510498 23.878063 c
215.081009 21.177954 216.391037 17.842447 216.391037 13.977663 c
216.391037 10.059591 215.081009 6.724081 212.510498 4.023972 c
209.940002 1.323414 206.815582 0.000225 203.035477 0.000225 c
199.255814 0.000225 196.131393 1.323414 193.560883 4.023972 c
h
244.513977 16.995380 m
244.513977 0.741432 l
238.012482 0.741432 l
238.012482 16.148182 l
238.012482 17.895418 237.559006 19.219053 236.652023 20.224766 c
235.795044 21.124802 234.585434 21.601686 233.023239 21.601686 c
229.343994 21.601686 227.479614 19.430855 227.479614 15.036346 c
227.479614 0.741432 l
220.978149 0.741432 l
220.978149 27.213709 l
227.479614 27.213709 l
227.479614 24.248959 l
229.041824 26.737270 231.511002 27.954782 234.988937 27.954782 c
237.760742 27.954782 240.028625 27.001907 241.792587 25.042873 c
243.606537 23.083838 244.513977 20.436565 244.513977 16.995380 c
f
n
Q
endstream
endobj
4 0 obj
8970
endobj
5 0 obj
<< /Filter [ /FlateDecode ]
/ColorSpace /DeviceGray
/Width 522
/Length 6 0 R
/Height 134
/BitsPerComponent 8
/Subtype /Image
/Type /XObject
>>
stream
xí½…_TÝ6ìÌÀ 1tw‡”€Hˆ„…€<E280A6>  ¢Hˆ€-¡"Ý­ÒÝÃàó§}×½Öž <ç¼ïïû=Çç¼\³÷Zk¯Ž{Ýë^±÷b±X[, ¶p>Äÿ:Ð®®T*Ó700”É<><C389>ŒMMMMLŒ<4C><C592>är¹¡¾¾žL
  ÐÄ!Qüç@e¡ÂGþèjCÏ—HÐü†rSsKk;'W7wO/o___o/Ow77'G{[kKsSc#C}™®ŽœâÿÔÁt¤*èJ$bXþ<58>utõ ŒLÌ­l<1D>ݽýŽ…žˆ:Ÿxæ̹óÉÉ/¦¤$'Ÿ?w6))!.6:òDhp Ÿ<C2A0>‡«£<C2AB>µ¹©<C2A9>LzHÿb}# k;KS}Ý?<3F>D }¹©•½³‡O@Hxôé3É—®ÝJ¿ÿ '7¿ðyQQqqIiiÙë²²ÒWÅEE/žæ?Éy<C389>™qëÚ¥gâc"B<03>z8ÛY™Êõ1\RÃï ’è[¸ø…œ<0F>ˆ p·6Ôý³j Ì@ÏÐÔÚÁýhpÄ©3)×îd>*(*}û±âË·º†Æ–¶öÎήîîÞÞÞ¾ÞÞžîöÖ榆ÚêÏß•<κ{-õl\dˆŸ‡“µ©\OGògïÏ<C3AF>®™[Ä…[÷³2³²²2ï¤Åz[êýAU…¡K&7³uõ ‰JL¹™ùäEÙ‡ªo -=ßû‡FFÇ''§gçæ<C3A7>…Å…¥¥ÅÅ……ù¹ÙÙÙ™©ÉñÑáÁþïÝ-<2D>5Ÿ?¾.Ê˺<C38B>×ÕÆL.Õùó¸Þží±Kyï«kj¾}ûVýéù­gÃ?§¢D:2c+g¿°¸·² Ë>U7uôü›š™[XZY[ßØØÜR(ÛÛÊm¥
ÛxT(¶676Ö×Væf&džûû:šk*^?˹“ÂÏÙÊXvH û!2p:™UÞ5<ÆÐW<C390>ŸèiüÇT“D*·v;šþäUymkïÀØÔÜâÊÚÆb[¹ü½óë×/º¹¢ ¸alm¬¯,ÎM<C38E> öµÕUåe¤ÅÉþì1³¸¿="C׸ü†ñUt°<74><C2B0>µ™ÎW}LþJéè›»K¸ö¨¤ª©{`|fqucS@+Hßùõ)»!<“F7HB¹½µ¾º8;>ØÓTUúøzb°¥ÁËD"šËÉtÿná<>ð´uFAUúK¹Ô÷:Í×Tûÿ>ÄR¹­oÌÕÜ·µÝCS «ë[Û¼]q£q¡@ÃMªðH`&þL%˜VÛŠ<C39B>•…©ÁîºwO®ÅùÛIÿþn÷ï2ÐÕ“[Y™ééàùo„ÈÐ-þiÛÌ6«7åÒ÷7©GMÿˆÞ"š8ŸË,­íŸ[٠ᤠ¼@Pü¥yਸØÍ]Á6WçÇúêÞd'w5ûãFˆÇºúFævî~A<>¾vÆRXý<58>à<aV<61>ÊB¥-õ½I;ú'ðHfêzâR~eçèÂú¶yCîÐœZØõ°ÔðTæG©X_ëùòôZ”§…ÞßÍ€ÿˆ`eÇÞ= <1íJÚiÛ¿yÇ(¡mF<6D>úÚa<”ðð‘ÔÔ=êzQ]ÿìšq‚Ú#nôtüT 3ÿLá`TÀ7ÅÚì`cÉ<63>S˜/ÿA\R­G`dÒ•û…e¯Ÿß‰rÿ½í sJ :Oøþú<C3BE> ±ÔÔ-ê櫦‘Å Úð×_\C6qTº`@#Ceà!Q¹µ8Ööö^œ—¥Þ¿¨D:Vža W3 ß~iéîøš—äiô÷fŽxB!É ¨©<C2A8>?…'ˆ¥&.Q7KZF—·([AÑtkbüÚM¼Ü<>{æªÊŽi¸a«T¬Lu¾¿wÊÓLú_/«ÌÜóÔ<C3B3>ü·Õ-}ÃÓóí/“½ÿæÙ<$Fð„YF à }$1þ·å®±SøõW-ã«`U„<55>4@y„BšÀìpAa*tÜ\aøûg€­r{mºó]zŒñŸ²Ë"2p»Y\×7:³´¾¥Xè-Kþ»góÄøè0žàûßæ "‰<>}ð墦ÑÑrtá†Ê~Й¥pµ°g€yÚ#aâY¶W§:^ßw”ÿÍsµßA,w;ý¤fhq “ååbOi²ÏßÎ%suPŸ'ˆeVþç êFV´dEºö¶¬¸ý_e UKVÐ×ÎöêDSQê1ë?DTy<>-êàs8PBoéEß¿¸Ä¸M`rÂ<C382>Dº&qÙŸ™Œ€|\c*
0ÂBz@;C%×><3E>»ËÃ5¹IÞfÆÖ«ØØûBiÏc¿”  „¿Ÿ'09<30>Ñ"ã iGÍþÞì…ØÀáÄ­w=óJj0j5\L!Yq@Ú W#TH \LúÀ+¶~|J?é$ÿ#¦’œJ žð÷Ë Œˆ'  „Tß¿9{ ZM~Ö<¹Î mÆnR€ ðÄ/vÿj'ò®73U¹>Ý^œd%ûÏ)A$ã÷Ÿû׈0mJØÙá<á?àÍ"Š÷ÿ2K( Â"
•œ€ |îð¯(eA ¸Uü¡)<LBlè<1C>ùyh…K.ïÅêÅÔjf\ìæ€ — ¨4®
๎ ¤°2Ró(ÞÃXç·™ÑÖu¤zú††úzÒ=§Ÿ`F•B=täJF!T† yû¨)<29>F‡2Po‡<sÐâ´TÏÀPnh` 'ÓÑÙ•£ <09>Êb€QY„cžœ´xÖ~CTº2$  ´jOȺŽ •iÀƒ³ô÷Aj<19>öªsf“Øk/°¨Ôx\?'•‰ë\E* ;[ó}ïnE8KÿÝáM´¥ÔÀØÜÚÞÉÅÕÕÕÉÞÆÜXŸxA¥¢Vðc8 L¨4:xgçèâêîêâdgef¤}ŠÝJ¬ƒà2 ¿”×½‹œî,ö¾Nó·<C3B3>éè²x©çî¢Õ“XØ:8#C.Nö¶¦†ÿáy,¡,Î( BZˆ
œL¤ Jdi눤Ý\œ<1D>4 üMÒÔè( U
C<>ÔÐÄÂœì,M<>sØïXB̃¯#«¼sphh€Q…ú<11>.˜Ô€˜Àž™Â 6<36>nrg}²ùù¥0KCªŠßt gdéè{:!átlTh€§£%v A2CsK+ÀÒÊÒÜDŸ"‰Tnaïv48"úTBBB\tDˆ¿‡ƒ…\¦¢=‘ŽÌÀÄÂÒÒÚ=üƇKXQEî”K??ÞŽô°A¬ˆÔXoߢ‡XG&7·sõ
Š<EFBFBD>O8}êä‰c¾nvré¿ÛÌFYô<59>­œ<ŽGñ²D„² ¡,†®‰à D ¨š¥ïoSÜ•F҆涮¾Ç¢N<C2A2>NLˆGÒÁGÝíQ¤Zééè™YXYZSQ,LÉ—Žž1*3Y?<3F>äãbc²ÿø¬®…_jqçÍÆu6<75>DU©š[3
Ó2ÁŒK<à"@ƒÄÛKƒßž^ p±0ü}Š$2#k· “g¯ÜÉÎÍ/,,È}˜~å\t<>»µLWßÔÞ3 88$$$8$È×ÕJ®MR"±®¡¥³_xBê­¬Gù…Ïžæ?κsùLT ”w"‰¾©ƒWà±<C3A0>ÐèÔÜoC«È \®yCïšßˆD LkW¿§/^Ïxø¤ ðiÁ“œ{7Râ#üÝmŒÑ¤ðó; Ll=ŽEŸ¿r7;·àéÓüÜw®œ=èj-×3vK,T<> HŒûåDÏÈÆÕ?<>åFFÎDP˜—sïfjbd€µñÛ8XD·pñät,ÀÛÑÜ@ßÈÊ=(úÜ•tÊ@aþ£û7.ž>îcoŠÐÚÁEúQ÷?©Y¯´nN0\ ~ n\*p£*4¦P”¿”sýõeÙ—bœ('ÈÀ~ˆ¥†nÁñW³_¼«ªmlmmki¬­z÷üáµÄ7Kcs—à„K7nݾ}ûÖ­ëÉ1¾ÖúêÚC÷Ó3s ˆNÍ((«øÖÐÜÚÖÚÒø­êͳ¬Ë§<C38B>¹ZÍ`Êìxéæí;™…å]S”)ämcº»âiÖ<69>[Àµ1Gm Ô‘"ˆX*·r;v*-#¯äã׺¦ÖÖÖ¦úêO¥÷/ŇzØIšxíaÑûϵM-mBY Yy%iIŒ4:ì¥p84cp\ZFAɧ¯õ,æúêò²ÂÌ˧Cè0ôÞ:É,="Î^¹IÕsëÖå¤ãî6ÖÎA§¯=|ñþK]S ²ÞP]^{ûl˜'Bk˜x<CB9C>}Ö:³ÅÚ‰Z #µ%éö¹ª ÁFÐÈ–¢~b€ÕöúÂHWuéÔO+Ö4{<7B>N`âwýÉšöïCãS3³33SãC?ÚkÞ=¹çïꜜýêSee®ÏnǸkÆV®ÜÆ;*åÁ«ÏÍ=cIGmÇûÚª_?¾zÊÏÞ˜J.³H~XR^YUÝØ3¾¼…\¶–'zª«*)Òçwbݵ¦s`·fT—<54>J?7u÷<75>LP¼3Óã£=-__çÝL
qµÐ?¨ F"3—c 7óßÕvü¦²P††¾·U¿yt%ÖÏ#ðÜs6: HNÀÜA]äD=„?}ãq<18>˜¦¦'GzZ«ËroÄs1ÓÛ“4-¢_É{<7B>ÚÊË¥EøùG]zü¦¦ãÏúôÄHwç§wì<>t5\A$µ¾ùñç² 8áÞ N§<15>ö¬º÷<C2BA>Y2E <)l®Ì<>÷³9€îá©9oê{Gg—Ö6èüìöÖÆÚÒìh_ýì䓉™ïZ~†zkž'5ÓE0Xj옔^üµchš¼CHÅÖÆêÒÌHomYÖù`'lz ¦"Òßµþ™˜]Ùd…@¾”ÈÕÄèÐàÐÐ`_íT U¤`ï†ÖÞ'/?zSÛ54µ°²Îs¤Ø\_žŸé­ÿäZŒ¯íîÑD¬â[yŸ¼û¾¡olv™ŽƒT™QÊÐ…è˜ËÅíD ÀA<<3C>%<25>f|Wß3<½¸²¾‰àŠí­ÍõÕJú]îå“>6eáU ‰‘ûéœÊÎdhp ãÓã´„ówkzÆæ×Û
ä|e~¢¿ùý£¡N&šsd"ç˜Guã“ä@íÐ<C3AD>tÒÉFeñ~{@`¤E 8 ØQn­Ì ·~È>hc°«"]kßS·ª{Æ×·hÈbÑ`5V±>O^î\M/n^Z[][]]<5D>í}=ÈRh4Ž±chJî§ö¡ÙUì% Çζbm~¬«ªàò g]‰<>˩ܺŠ`}S8C…n¬#ʵµ¹Þ<C2B9>·ŽY ëÊí2J¾õŽ-¬nb~©ü¨<C3BC>íÍÕʼnïõ¯³Îs49`<60> i럘QRÛ7¹¨:Ơܢ Uܾ–ù¶{Ž<>Ê}ë "‘®‘}@bú«šÞñy*„GŒØHzkua¼¯¶äÞ™ JZÕš€ÄØë|Qëä
²ºº2Þþþqæ“÷­#óë
ÞÙußXžê«~z9Œú$ÆÞçÚçø©J¨¼*Ð3<C390>g†<67>Œô€<17>¼ÓMàVŒ6Ô€.haª§êñ¹ë=²HÇÐÆ?)ëmË0²Ï<8 <74>—úw/^~ý>¦N6«ƒ•·ƒ­x£!¨}hjaõ÷é•-Vh
K7ü)ksýuE×Â<C397><C382>uå®ñ…-ØDŲ8ØOðI†_kÃUwCU”€]ZÇà <0B>>¶S]Oæ…@F¥bcq´«2ïÒ WS™ªJUéÈíŽ]ÈùØ>º°AÍÿt<11>¡UdèÍWÕ?Õ»Òo±©Õ3ÀâCs>uŒ"iV"îߨ<C39F>ùÑŽò'iaÎ x 1ñ½ˆEt<45>J¤Xjüôák×$í( Y `h}~ þùåã†*¶¬ktýÃ<C3BD> Þ–¦˜ªz <20>™žàG ÌDš`¸‰<#'³?>ç$ùX¨iA¬oíw6§œ²Ž><04>Ì7tåæòhWC]çè
(׆@ B£‰õ¬/Ô,l(UyÇ<79>ã/HªÃ /¯…9»Tàþ9v@ é¡Ö|Ç”ñ™´ü/}Ó«˜\±è<04><>hauögíkQn¦R¡JU<4A>Ú_ÌûÜ7µ
/24ÒQW×5¶Â³¢\Þ3:€#8†¥PÒ,<Ý,£àG̼£X™ùYóüj„©)ˆM|SJ±LB°ÿ;ý³£«f”ÈqÀ¨ÜZ®šd£/Ÿž}øýêÑu8ábÊž¸É†² Odp•4\%S¿_Ûs}å÷c\wmReG“r*ûfÐ à€OJÛZž_™àQ¹6Xy+ØŠÇ cê™ðèëÀâ¦R;³j èHô`{K÷ø:ZŽ8wù£IÙYe”ÀÉáÒÓš<C393>y<EFBFBD>œ``©C£[¹±0Üøêf¤Ëž•S±¾M`rþ·Ÿsl—*ÌdP¢,Ccs¬¤;‚œ 4 AÇÈñø¥gµH#  6y,¤‚É/ 5¼¼áb¢SˆÊ8% <20>Í噉©yhTfŸrsiè[îsVdè×4BáIÅ<49> P´¡²€F½4<q ýèRA0*7f;__±ÑÚ¤KÍ<OgUôÍòÚÑÒ p;ŠõåÅe.@àiu¨
”ÀM¬owâö‡Þ,•"UøG<2€†kóÎú:z<>˜˜Æ<CB9C>¸ªxÂg<C382>D<43>´guƒ,AøÄ…h¡h½k´ùÕõp§½Dí{&÷k?íë!.TFðw”e 4O64w%¨*C}hÚ³ú!¢lq<>¯Ý€Äµ<ÚT|-ÜQ³©§¢æ¢ ä!5 §‡„§<E2809E>MlfD9{<7B>{Ù9Ïê…ZQ÷«<C3B7>¸Æ¬S¡©÷ƒìÔ¡©Y LC<>íµñ†¼$Ocuý¡¸œLß=#d<>Åx¢—¬\ЃÝÚPÕíÎÈuL½Ï>mš\'<27>tANÜÚØ€¸¾aµ¹ÐûþÖ OߌTb ȃºü .'ˆ$èÖp—„96âÈ@eÁè‰íöÑÆ——Bì Ôí<C394>²˜¸ŸÊªø1O²8ó PY BcZ¶¶¶1<>xÖÊq”…µCKDÙ¸Ø •<00>@JºéÅ¥`$<24>@&'€àav”ôâ" â8Hå6Sý-Ù°&1ó¿ô¶o™…Óy§["¢GÒ¸ŽÖÌ ̪gTÁ«LÃÍ,qïl/þøxë¸<C3AB>š)ˆ lC¯•´O­ñ@þU"ýÆP
ä:Ýè¾D Œ'ˆd6¡·?õ %l,ÏMŽ<4D><C5BD>OÎ.o@ä ;åÖÂÏŠûÑ>~ |tÈ^¥ª¡¡©¹OÒãjpÔåß¡ äææÖ"àØQ, ×å_ð³ÔŒ×Cìô¿í\Pˆ6×!Я®a~ÐdE*ÍÀÉ̽Ïä"iH' 6¸Ã³r{kKH@
Hú¼Ÿ¥J`%¤¨7Öàΰ­ØX[Y\XÀL”… \þú Ú…1]«à›kj7f@:¤’.Xp€ÉFAS·¼
‡'\ ¬$š_ì†í/lB4æ%¸Ë)#€H×Ì÷\AýØ*ï%P@« ScÃC˜ü/¯±9"BP¸ÃHr§±¡K\nýõ s“™Á®¦Úoßš:û§7AZ;$æ·–Ý ÷:ŸÞA3sÖPÑ°ÂŒk©¿ŒF,ÁÈ%ú^ù÷y64<36>ªÑ•ÅÙé©É©Y¬,`bkÂÎÖbÿçìÓî&*Q+})Ï[&×((•±o®ÎO<C38E> ¡,óK*AH<41>â JwŒ¹Wñ}<7D>LSVf§€ÙEöz·¦¤V=ˆsWí¨ctÀÜ<C380>QªY”˜rNÿèíîîí™YÚ`(ÃÖlç«T?Z<>ÉlÃïU<C3AF>\yPn‰
€J »4óK<²›;ÁH<“FÕÀtìX °ß^ì}{5Pµ 1tŒºWÙ¿´Wè`yf¸§¹¦ª¢¼âK}Û<>ñ…5°iTED<átL|_uͳBÌ<ÙýõÕ“¬ûY¹Å-C˜…!®©ï e÷Ž:yÄ<¨ê˜žž™_Þ„
<EFBFBD><EFBFBD>å…¹™i`¸­ìJ ¥1èà+%<25>Ó*¡“øõÅÉÁž¶¦Æú¦Öž<C396>‰…5HŒ"€Ø8×óîV˜<56>°ò-»ÆfÂä Ù%”þ`wÓ·ªòO•_ê;~¢, ¸° 9A½ž Öw,혡¡…:X[@Ò­<C392> õÍmÝý<C39D>™Õ][¹9ÛýæÆqUÒD eŒw°ÇùÑîÆÏïË^•”}¨níÇ\<03>¬nOoüè9žÌ©ÜDA¨z
ðu`…K[a.ˆ ¸TàF<C3A0>3 ¯"(#¨=©…jq;펨ðsCm_Jó³îܸ~+#çŇº¾qto
Œ‹À(<28>KŒ:A×>üd»'ˆws¶§üQZ\dxĩ䌢oßgVg[Ëó¯Ÿòµµp ½ú¬²¡¥¥½ghvMÕ­·±VÑÛÞÒÒÜÜ\ó³[3Œôž‰OêFX·ИKß›*Ëžåæää”7`­‰äz†íµ‰¦§çÕ¸Ì:øjY7ã&ä½rf µêU^æí×o¦ç<ÿÐÐ7¾‡Â<E280A1>;<14>'ðæÔ1õ:“W?FIÃ…ÆVSEéÓ'9<>×÷h&y«±ðœõl@bâs$F¸vmæ{Ý›'éWS.\H½žù¼²cŒRFÁqsª©0ÑC.Æö“ó)>uà±T&AG^UF<71>ÊBpƒF
ÌÌÒèæØši-Lò<4C>³Ú:Eg}ÂØÀÈ ~²çëËÌÔøÈР à°è³7rß5 Òœ
®´²$ŒRãw?ÓlP®ŽÖ<>ÌÕÎÖÎÙ72íIU×à@û—wφºYXxÆ\ÎÌÍËþ¶öç,zÅ·³1÷³îÝóü¼¼¼'9w/„Ò ;}ã·ÞƒAÃqBÒŸÃmþÝÔÄØèèØÄ·Ÿ¼­ÿ9³FÒ+¹+*ïE:ðž‰%߸ǵ£hIŠÊòx×ç÷RNG„;|<æÜÍ'ï‡hZ<68><5A><¼@ ŒÇ‹õíÃï|ü±„å ¸™Íö7¾Ï¿“x*o?~ƒ¤17„3.Ƴ <75>Êz±ÙŽp¸w°¤ÐS->,Ð×ËÛ7(òÂý²¦Ä g¸+z^§5×9"³“t*ñ‰9ÿäÆ}@Z†*@Û,€Ù€äÓnhY<68>Ñ+ûÊÒŽšQñEºæGSŠ;fid”Še¬”>L<>òsµ·¶´°´
Ž¿õÓsA#…äAb”Ú…g~ãÌí/åÒ<C3A5>O·Âì<C382>ô¤z¦X"|øösEqÖÅ/c™DÇÀÊ#82öTÜùÛÏÇ„]é<>ÕÑÆ·Ïǧ¢Oø;cÅPÇÄ#© q¤B!L÷}yv31쨻³ƒ£³»ïñøëù]ãXƒ3<lδ%ûšÑP%Z»ú¦gDDØÁ4³õMVräQW{+K +<>„[E5ý |†€\ 1ªF6jöI!<21>¬Mõ~yv I»9;:8¹ù„Æ_˯ì¥+îccªõÅ$M=Š(sæ€JÂ\½÷Óƒs!n¶¦ÆƦ–¾17KšÇÙrüì¬ TÜ ±ÆìAîq¦¨c|?8˜†Güx{BƒAÐIc7)ðC‡ÆÄ<C386>Pp±@:ÝÓ<>r<E28098>½ôí#îÑî8wP¬N´½Éˆp47Ô“êêèHõM¬ÝO¤æ}ù1G,˜;@¸Ã£]äƒÚqN ¿¶¿¿½d…åKìSÚ&ÝÎÌH<C38C>ñ³7¡<37>;ìl˜ÑéW¿SÂdí0Pq/ÎÏÅÅÅÙÙÑÎÒeV!×?|_¢^‚ò+×g0à$‡yØšÊõõôô MmÜCÎd¾mWµÇöÊ@esÁÞb²õ:¸ %—GKïb/ÔÌPOWG¢+Õ7¶ö8VPÝ¿ÀËB 9<<3C>(<28>æA·>aWÖ¿½6ÝSž“æ)$mdjírîÁ‡.6ÉÄEIWÜ ·£¤<C2A3>HLÁ˜œ Ó/ `)×ÏÆH¦#ænQwÞt«:ܯ<C39C>±o"íeGDFžçŠ;çLp`ÂE€¥`R<>=”¡r€"Ì‚ºUÐ2 !ucäËý¶ô¶ºÄÈ#©°ez“¹@éz—ãeaÀ^T<>Df├÷<C593>f÷ðB 9<>ï;ˆY5œÐlËýéNF „3´õ ;îïl.Ä…ú<E280A6>ééëËmÒÞö¡Ê(60§·Wl ôé#£:bs>õ¨v­IPnÎ}¯x˜`g¢ÇÏ>ÑÁßSw_wL¨d<C2A8>Í©f uF`p:¦¾É/;æ8 oŸj-»uÒ<75>ö®Y°Ø åR žÀ×Är׸Ü6lMI—g'ùÛË@ÇjÂÈÖ/>ëcæ¨ _“ÍìµNðaî€à4=k~~ÑßJ½½#šxœ~ømDµ¿°9Õøø”³<E2809D> KJºø™
¸ it£ь̤‚úˆ`RÏü˜Êã"“@9u£ô<>E]‹À+o¿/!+ä XøY•çi&ÓÚfÓݱ”gàèè
ŒC$'CÚ…Ýû:
~JQï@|{êdf Ž khnçHçÌ´¢k€h•Œ­$°Sî¾&ì?ü‹¾•\‚Ébƒ»by¨úqÒQ+4&ÜD=s<>˜ô÷=³h2*ÿöbß»«AØÎÆN?út?copÚœíý<C3AD>íºk_B¤+w¹\Ô2¹.”E%'Pcê˜ù¥t/ iÂöÒÐ×Ü3~ÖZIÑÑ·ôI|XÕ¿È iGA„hA<15>¹ñ²Ç½<C387>>q7Üž "©Uà岞y¤ ÷_
Hj nrP7jD•ÎAm"`ç¯}«Ê •À÷`h¹þᙢ†$ù$ÆY £g~ÿëÈ:¬a¯„8\pÎ×\»òPì6¿ñºÓ Ä@N ž@ k|»r #ä°³µÐ_]pùäQ'Kc}™Ò<C392>žpvMêD €Söû<0E>þCoW ²Éœ×'[_¦Z«vk8D2S¯Ó¿ ,S­"åµêL<C3AA>µHdà“S7!(—G¾=Nð4%¥HÁ1üÖû^0îmgQ% µ=~I“ªb}²åEj 6mᢆXÏÊïBa]<5D>§µáêûöôöã ª¹ÃŽb¾óe²<65><>
¹[|^Ó$r‡ø)æÚ_œõ$JàS²C8Ü9<04>ùÇ¡³‹ÝjR<¨ÉDæ²Ü‰9+fZ
°2ô¤iŠÓø/Å|ßû¡¶êÕGtŒ\OeWC˜ø$JÄ ó€KoÀRÈ5³>÷³öÕ½”˜c^NÖfr:<15>JÚ ô‚ èyF£ÅÞ²ÞšÓO[£²Ö¤·—úËïF8hi#ˆdþ‹Ú¦ÁŠ¨ü[Ó­‰nr±ÝóvÕùD¬Þ”\:à]<13>„ܺ±5d—@<!•½EGFr°HÆr†Q£¿<=ÜQ½š,€V®¿í¥
<EFBFBD>© ÞÕiˆ1(kõ<>b<EFBFBD>ÐÓ´ ³c}ŽA†™ï*>ïe$ƒ•`Ê\ã± P™ÎD<00>üÒ<C3BC><C392>;pU Á•)\ÓÜŠ¹¶§ n(zèù—<C3B9>s´¼ˆ¿1Ñ”—è¡Z6Ók<0E>—J»ÏF<04>1w`+Kb#ϤgØX-9ca¸ãsqöõs1¡GÝì-ŒöOKJ(íæU†IïJ ï;Är<C384>¤gì´¸9Õúìœ÷®®Å 6pŒ¼W5¸BV¼ØSšâk&‘€»—‘BP®<50>Ô<Œå+º» Ò³>výý÷E$AØY¦ÕfšF‰ Æ?Sey=?<3F>¤÷är•[<U©Aqm/t—$û%cTcMJ]š†Ï/éa6$‡i kyìÆÇ~žç<64> *FÁ7 ¨h¨Ðq ýWÌ~ üQ€¶YÁ–<kc{®ýy†)/½ý.ˆËÊ•ÁŠÛa¶2d{Ø¢r¦v,{œO@ŽÊú:LÒ4å Sx¬7wÖ~(ʹ“šàngj°g| ž@##âAL4:h?Al
&«vÅ
X&&ì»Â3 Û)¥]sB«­üütý˜¥®®ªz@˜Ÿn/}˜Vw<56>dä§-Óª©æ"Q<02>¬-1lÁI¯Ž|ÉŒtdÓ]Ii1À$¿¿Pc??\ ´ ÐšÑÕ°ÒÿñZÖKµ<4B><_zö Ⱥ½„'69šòÔËÚé",ÜLçú)Tè¸Òè†=p±[®Ò »ÁÚŸŸ¡¥%Ëc7ËÕµ7ß]šÂje°c(|!.š;” µH+éœÁêœàˆu¹åÙ±Ÿue…÷/'†ù:˜î>ŽJH.A[“g,lôî~k^Ç<ðꇟ+<2jÍ+<2B>XÙ<>ëi,ÍaΟ¾Ü=n#•Ù„e|V¹08´=?ëI3Š}<7D>9DdUsÑMÎFTt,XÒ”1P4äJ÷ïÌû@úèþTwCmPèÜŒ„˜GšæGŒF ð<>=*¢4d“‚) QÀǨ<C387>4P? õÃn/ ¬]FJj<4A>Ü„ä•.Ü*ì€'¼À"£øˆ®Íñô/¬VàNK<4E>p÷õú­Àňùaß<61>Qz˜stf%ÍÐá v°O°4;1ØÛüåMAúù[#Í^!*<A T»úm8Ô*ävÅІZD¥˜ë(:ï¥åª<C3A5>Ì>2ëÛã549׳<C397>|P3™øµ9Ùð8fÏH-@×êØ ¡WÃ+5£~ë<>;Uƒk°³DH$ Éêt^ã±#$³1V“a.
9í,03ŒW4ödO.å(“W¢b%lt€<1D>5(72rtºÉ¬2iõMöd kðŒ 7ý8T„ ZAcÍ%‰ ÆDÜGíÁ ؘlxí¤<C3AD>\ïƒØ˜­€ð)<Q挰…|ô\~Í[mÀEÀ:/íàM<C3A0>ö5U<O?âb.CM @…`t`sT/RãŠ~<7E>þudQÀysº9ÿ´ äÚý<C39A>Z…Þ©Zã±lMÖ?ŠvÔ‡°ù¨~çcg}äëýp¶b²bÓ£i¯û˜ ¯tŽÍ"eva_iY
;[SMON9´HÏ!2K(ÜßÆD]ÎIGLÇY“`‹ê]€<è‰êÕ†Ä]€Q%\%£x­¡(BØIÝvÊyƒÆ=jM<
1<EFBFBD>h%ÈEXŠÌ†¨+¸2·?°ö„7Í©' <18>T<áˆÄÀ6˜ù¨·f<18>ÒöæÚÒô@[yþUú º±™ÄÈŽ~ „=<Á6<ëÛ(Ö¹¨X<C2A8>ÀsN²U<C2B2>}е ¾ÁF5*ÿÖTcn¬³<C2AC><C2B3>S,Mƒ(q"ÖÊÛªS{ Â<öU×ß>`Œ\ „ lfs¼.;ÊþÀ¤!(€<>ï@!·&ŸÄ:£ó Iѹ9%@ ëx Ž²›¨ ¼êæ0:@8¨nJg L\UC°=pBÆð#]fÒ(fœJ@VõN>¬›%Àig}´úwýHdàrº yZA<5A>±[À(<28>W3<57>=~ùÙ·Ót<18>`2ÃT}izœ<7A>æãoT!ÄF™ —{(<28>8<:EÀ(3Xï~èXÝ E$xCi¦ó°`gàû¤Q<C2A4>ÅþòA<07>óȱÇÙ—àoä <0A>“äŒ"6¸ ð£5™ŒëUð­*™(“[”4 Bà 4:À
 „gÜ15Ó†/,<2C>¦Œ'€ <0C>12ƒÂb„Êô£K ŠÙR 0ã"<22>=I%]a ¬'ˆ<>èÙG<C399>¸Í(<28>¯AïD´¸|Õá3t8•Äˆt<CB86><74>Ž§>)ožhAŒ …Eìhežö6WÉ
˜·BNPUÈbÖvSBv-Q&ÂþZÃøps`7œx“[ ß4å<34>r1À:5ç »³:ðñúo()\¿Ó#XƒÆj2ÃNìè-@Áˆp
”@Kt˜y𹵉ag<öP£Ã«.Úb”˜¼^ò'ž –'ZŒµÒŠ’vkð ýÌÌ°ÛPÜpCe7©Û³Œ²U;H;Tó¿é4:¶ÎnQhHŒê]iHjìpìLFñ×Ρ™¥5:¦ìú<C3AC>ã¦:Ìx_O€+ç ꡫ׸܂õ™ñºQئ9º<>,!ÞñD¶¦ó´G”P~3ø7”€5à Ñ9A  ETEVí¸0:lŒ×fGþf`²
½ EHzª)—-!<21>'hÉ ˜šQõ·TpŒÌ£9%<25>œ€`­3Óø ÍH`FzfO¤ÐÝÈŠn®¨À=ÀŠ˜hŒ¬ŠQóê$Hh“õ9Ñ$þìÖïÎa
aQ ÂZ”R<>ÛøD¥>xUÕò}”^ˆöô:Ë®‡ª÷ BJ€#(+3Ü… µ¥} ˆc(?òêÖûyâÞ· .^Ñ<.7`fIiƒm}¾{œm—î$FÒ9%OPË ÷¿1‰×ÖDC.[Žß™Ý‰L~Þ ØÄb"«2°y5%0ž<30>ä~%«(<28>ð´@ ©˜;PÛ ÀL`„J 7.Ú <6E>T€k¬B8X,¸Ø-(ÌŽ hœ@”@spÈêÀÎŽbº¥ žÖ ÷Cb†÷,rJ 9A3:tôÍœbRî=}WÝÒ;49·BÄÀüÂ÷öÚXCáY¬×Á#È“)šEÂù/&'ìú2§.¶x‡Cž·fèZ XÐÂÊ0ú"ŸàèÀ2}‡“9$ó<>Øû7ð4Ñ~`ÓM½p E ºÖt膭™6,¸8ÔwŠyÔÀÆj²#í0 'Ðù)ª÷ù™ßPª­E<<3C><><0E>ø±º pWÕ`La*êFP˜†ø8`<60> ¨hÕùT¹@¥'…0wÀl,äV9qY8`Û¡«8Ù[«‡j71'_ž€Õf-J ·Ô-œü"’®Þ/(«lèœZ\ßbÔJ—bé移a‡ºEFà  ă
aë Z)B¼®^'ÄV_±PØï<C398>ØÈë ø"7ä/<19>o°™¦ÕY§šóãÝúWÌöD†ªÉ‰<12>-3ô£ˆä°<C3A4>émªÿAIKŒ½Î¾ ¯Ò|\¹>,,+ƒ„•%Ô/Ä°(<28>äðŠÉbt@ü{(<28>jl?!ÏçY昢5î¾Z€3ó*ƒÊÁ,@´\NÀš^À•wß—aø•Ë??^Ö v!úaí‰Sä#v x Ò‘ÊÍí=#ÒîäW6}ŸXTO+Áæó±Ëš…º†0: B°è
9<EFBFBD>90HL<EFBFBD>¦òù5V¥c2³»F RË «|/PB&¸<>Õf)†8?AyóÝ%© ¢}aEbC×ÓOù¦ ~à \NÀ¶EÚ&¼!†<>•ÁÊtˆŒûƒSÒï~,SÙ<53>C,t#iÔ$F¬U 9Âèð£œ(îÈ3§ÆhmVÖy¯p©@ÍÍžqã"…R=Ã'32€ŽKPxEtA:ž!'¼`'*±4Æ$yX"¶õ±ÚœØþK¤cæ{¡¨C¹œ°bÄbú(±…½»ßñSïä¿kø9½*ôYÖÃT<C383>*„FiPx%ˆåžgÚ±#€ÅO6ç'íߣã,1¿<>û‰ˆ„¼D×"à2§jÅ
5¥ œhA¤kîŸVÒ-,<>Ð$ŒH<>dq„$]<5D>äa6Þ $]3*$<24>ÅDlPhâ >Q,$ŽåšýÚŠ'ÀòŒdi=ÁvQnŠž_Ì ¶üA <ª` 1qhXtµ#3€}<7D>'`=<3D>Í Z0 'ìlÍu¤Xîë ìý¥W °<>*„§¹ƒj<C692>A$ÖÕ7251”éÒÿ¸Ø8û†%Þ,¬ìž\Å'üÿú…^«N 'h<> ]” ÒwŽÍm@‡¥p;ŠÅïoß·KNo5\z¤Òcc[ 4¦KLДꦜj~zŽÎJ#€ˆŠØ ^<5E>ø1:<K&qO!q²êR,}GÒüdšÄzA—JºæXŸ€`9Óö ÃBS“bÁŒ ,—xÂî°Ô°ž°<C5BE>ØQp$æg®5:€÷#YüØ…*~*•t¦3…«ðKQ¬º <C2BA>° ¡G²
á}äçAÍ(m2Ê•áêœ8w“][F(ƒÌ: åEëó…„HbÔš;ˆD:ú¦öî^v&`ä& Ll=BÏgìá/ U!¿e…óù u=¾è
 k¼æû?0ˆ‡eTaG2p^£ƒä¾æ2vœI€ˆ<E282AC>Ëù: :cžÖÇksbõYS2*BÄ(âÒÏò{']Œö<C592>Ž¾Í1¢"48óÇxëÕØË¿‡=X#qåúDCáy_ Õ;N"™™W£êaFGHzc¬6;šM6Á\µ$FÃöQÂ^ž@£ö³°Ú,cqjÚK&Àu¢òBP=’ΠÒÕ@1Ha Ä ¶¤ À@ž@Y•bÙrmÂÃÆ\ÏÛÛNFÚ¹"‰ÌÂ'ñÑ×AvÊÞÀHbTQšÞÈÆëDüÙÓÇœx½#„±<E2809E>ÿ™G_‡ÀE1ä7¬G;rJ0ò%ÌSˆkéçûÝ[~àÝ—^cÂÊ<C382>žù³23ÎÃTkßâý<42>·Ýtz ØQ®ôºE G¤gw"½JÝÛקÚJ®wØS}+¿sOjGè<47>1ü@¡&a<2Sà•·}HšA±ø³äÓ5$2S·“éúøwÒ 0B°ºAIÉNª0V'Tï#–ÔÔËìe}Œ— [`µ<>€ìÐ0
|\‡Î 6
 ˜à ˜¨Ÿ И`¦ d ŒT /;è œÐ 'Z^^ s6U­‚ñëXúÄg~ê<>ÛÄŠ…¦•%Zcdí‡<C3AD>ÁÀÂ5äÌ<C3A4>'…Ù)!Â˪"‰<>Mȵ7ªI;–âce€Ü@ g^va…QýÅ^ŠpГˆÑëÉÎn ùØoF)P~á„­§%}ô<>Ü}s·ˆë¯Z&°ÀHÀ<48>Öþò;÷¤kî—VÖ³ÀÊ*Z©–êd¢!#”Åúhb6{Ý<>yB“ÐWøØ™%l«zžÁHÉ߆ŽõÒNÚÀÜ=êVYÛÔº*éYlz39†Fö/^½û)<29><>äÇ,2ÕÏ\,ÆLªD<C2AA>ŸÉ<C5B8>+†JådrM.v `F²‡Šµ…ƒ=Ðqí ­È*IŒGÄX™Ã*ãµ2.ÅòpýkžÖÆ2‰D,Ñ‘ê›:$f½ïšbg÷@ ÂèÀÇ€¸ëùê«ò/ÙòCJ"]3¿t}N ØÚç‘Ü-é… b-oªñé…[cC}}==$‰UO»•ì #e{{u¢íõÝÓþŽær=:t/“9øÆÞ,naÇ“q)×Fª³±®„¸År×ø'ªáû‰‹µO¯„{XÉeºÙcЙìOêÃÉ;ðoª ép: Ų†æ\<5C>h“žàï`n@þue†fö~q·KÇ # ê7#ß²£Ù Qç ñPªW D à ¨hø Ñ£»aç_<C3A7>CŠ5¹1×A`Ö¤ÐÍB0ГZO*#Ü dÅo”ë Ø"a£ƒHj|íÝ÷%Ž9o.׿ºpÌÃŽ^Ù01·vò<76>H¡¯­¨•S êÑA¬kdã™úðMý<4D>ññï_ž\8æH/èJeFöÇo¼í#^ÿ´ˆ<C3B7>§<EFBFBD>Q]Oª_š&œ}.ÔÛÍÅÉѽï€iJòËv ;[Ëãmï^Œòw³·±²¶vpó¼<E280B9>õ¦y„vÁÉËÎÖ|×ëË4@È8~ûÓÏžSôêÅ<C3AA>º—wâ<77>¹SYŒXY¢Rs+zfXYxÅ€'<27>ÄH”pD×ìèÅ—!àˆà;Š•‰ŽS"ý(i+;—£ÉYoÛF‰©Ëí(æ{^_ dI%ÐÂ%ëñÿÉj3(£ƒ˜½ï€ÍDÇÀ[á<00>5óD®hÏüŸÈ# ¤³Gn ì€Ù’
PQ<51>U‰±Gb^ã„Šá)7FZ>ä^Kˆ8æçãí“œþ¢ºoš*¡@ ´ž€Y×È>()ýeuÏØâúÚÜ<C39A>Ïy—èå)k{·àóOj†°<E280A0>ÇÂ`).+KŒGôœhv@kt¡ã­Mu•çݼ<C39D>{ò„¿“™žXlàxxG±:Ùûµè~ZBTXHHXT|Ú½U]ãüßp)WGjrN¹°¿Eº¦^çžµ°¡<05>ÿR¬ß=¾Jeñööõ?“œQTý}†Ž5 r¸P 'ŒÊü2Hg¦É¤ÀN<E28099>§¤<C2A7>GžNÉxñ¹{b…<06>ôhÍ£874£„]<rT PB2F9<Áß\}Ñ3/;©Ãp!7¦à¦'h°&º#ŠJÅMPé°ŒÔû™‘pkæ —%ˆôlCo¾ëFW¸n-OöÕ½-¸#-99åê<C3A5>œ—­Csë 
 Mú¦
ñ±¾UÀùœ<C3B9>mÃskŠ<6B>í<EFBFBD>ß^ÞO9vüxdüåÜŠ^u÷é¨vDѳªG阀†žê«ÿXü,ïÉÃÛBäÔÜïbQëô:{u¦Xêø\Vø0ãÎíô첪¶Á$'¯.
<5F>}xzy¿°(Ž°[K½µo 3o\º˜œråΣ—UmÃóìÐ¥PÖ9ÁX£ÑRCJæH Â%Ýö¹´ ;ãöíôy%U­³k
<EFBFBD>º•3<EFBFBD>¥W¬…¤±²$P°ž°—hWšñ* äžgh?<07>¦hAx¦> ÷`MªqÃ¿Ø …ÿ¤ªÌj`a\ŽÑ1öHÈ­Y±2(ÿüX_sõ§7¥%eï+ë:¦—¹pë<>‰'ˆåN'3>tNñ¯®moÌ ·V•ä=¸—žñ  ¬¦wŠ1äå~w5@8<>ˆýýå®.œQÛc?»ÛZ¿½}HïJ£9"Ó?}_@<40>šÞâïm©ûú¹êKmKÏÐôm~#<È_±Ð_™yÒY8 éÄûlaã8¨îðÁbge)+EYê;¦Tea
"%¨x±¡cDƧBÒðƒ¤§‡{ZjYÒMÝ<i0V*–ú+³¢97B;ƒ'h¯1ª:šà \N@¼L<%ˆEr÷¤´²À<C2B2>9Ae
݉iHžtÜj<03>²„Kj<>ZSÜ÷.  ,«*J cß—KÚ§Ðí©ÕàA¹µ¶85Òß×ÛÓ÷cxbn™<E284A2>)œ<>tŽHä.±1»†<C2BB>AŽToƒÝÍ5Õ_¾Ô4÷Œ£€-`æÅ9Õºë˜ú¦¢÷ cRœ˜l­//ÌLM · ‡aáV$5óJÊý6¬æÂtâeeaz|txxdbz~•g‡°³½2Z—ÎWxsµ­ozãMÏV¦à öµÅéÑ<C3A9>T¡‰¹¢* "â”ÀóˆÔÔëÌ“šaHB(ÓÊüôÄÈðÐÈøôû_ØÂCÇH}Á9Õ[Bœh‰OøW”o!'HŽ¸Š¦%9¦ï³c
Sq¨\aaÕ <óüïQ4&€D¾ÚL<C39A>¡gêc³D8Âu¿µ¾²´¸¸D_8U"óH ?\p&J Ñ<C2A0>º¯cäýÊAT,Éu{cy~jltddljae<<3C>HÆþúà$—±Q#ùË@,ZøQ*··5ßTÁ:žMPZQÓ8q)øAˆ>º¶¶¾±‰þÊlvg{u²íÕÕ[Íš²Ž±[܃ªþEa…>Ž³¾²¼°¸ˆ²;¤ts9<>Ï!iÌàÄAá7ÖÖ×Ö×·(l”të«+¡ê¤1'J BÁèÀå^^%Ðè€Ò€@€ ¬ñÆ«ÿÙ˜7*mh?ÃL1¤¨nét« e¦>N<>tT:äÆx)DRSïDþ!=ò@7©h %UžéBc¡/0ì³Y$º/}Ó^¶©?BnÔdëët`Ï„<C38F>͹®Ë<E28099>ر#@0 ÃÒ5 P2k#ÂWøD:†Žá7ËÚ5Ó&<26> dâ÷Îö:¦üw¢œ<C2A2>ø°CÉ,Žž/¨¥/À<>ºüJe!3ž”(
}<>€Î©E GtäŽa7JÚ&ÖÐ6äHL˜”o:ÞPÒª€Â²1Ÿ ž°d%¸œ.h™Q°`žT\ZàOð.sbÊn<C38A>Ý Ì€ò0 «ZìKÁ/¹ –Î'«<h%“\[Ye)P­,¡ÑtäÎQéiÑ ÖyU)Â…±zeàëƒXÕ€
AÁÔ7ù#uQàoGóeN®‰[ôÝ·_Ðpqà2ÀtÏ'¬@jÿÕ™Hl`r¹¨þŠ¹€/Ü Ì@V`\«+«ôÕ#ö„&<26>ĨÊHט^ooŸàI3h ,k“<6B>ï2Ny˜ cñPñrGõ'ìÙÉ#J(&ž@Q°¹ö°Ïr*¯IØ÷Aä¦<C3A4>êöûÀ«a·ƒð$h£1ThÜâå­12H í‚/=g!„\AƒŽÞ·:? †Ï§šà l=<01>FczbΗŸó¤à‘.m!Œ5=K>:G 1%"!F#äþ4ß^C´ô?³÷ÞµO =á¢*BàVn­Lv}ÊJô±à»ŽÜéÄõWM£Kšo$h—Ýyevbljì%@NP7vÜcÒ߶ažÊ<À ~tQpD±<Ññî~œ·¹fW”€Ñaþ¢Ì´ó…bÓ¾àŽÜS²à GôÙA«ƒ)<29>žè™nJWƒ;ÄüXÃ.¨É@731…F O`Õvåym?Ÿ-ê4¨ô©m-½ã˜i“5ÖˆJ€ˆ>÷ëÏYêc䈬@…Nù¶7—ÆZJ®ŸpÄ{äÃ>u· Ô5ž ¦Œ×^qeÍà <0B>ÑàeÈ;„ÈÅÑÖ÷™‰¾VªZ€Hjì¸<C2B3>ŽÝÃ#yW<79>L(ËD_[K퓈À4qˆ1wËZ<C38B>´j€ã*øÂöÆÂhËÛŒ8oÍ<6F>mF lÉ
C”€ÁWU½Ä˜œ€H%`î ÉN>¬çBÝ*°Êã
S °a€…PÓ¤à‰À4(¸ØÍA#u7Ø^¤VVQ}Îô±â^6!„o\L<>YI•ÞVõúMíÏyZñšE  Oªz& R! '€JnE»b} ”Âoý3«Ú‰<C39A>0:Þ$zž17¾}ŸF¼ä‡<C3A4>JKÀŒ`e¦¿áÕÝ8ß=oÔ"lE\}þí»=¦€V\i©xý¶~`õCìJÓn¨%€Ì<¢o¾¬ý1½²ES%M±ÑW¼úëïÄzi½Ä<C2BD> ƒ'R%Èhe‰"+¢Ñ<AfO¥¡=[ÐTmhaÂ¥ÌX .v Ðö(^±²´[¤)8'?ú@D[Ûc ]m®L÷×—<¼ûàuëøêæ~ ?>Ý8Æ)ሎ¡]йìw̓3ümtQ@\Ûôýž¯Ï®G¹kTŽÜ!$5¯²sxò<dKÂöÊÄP&'$úæî'Òr?¶±ÿ
 eQÒ<51>,­Î·W\‰ò´TµD±ÌÄ%,5·¼}de¡ BùÁHÀÜêŠÜÍ~×1¹¾Iؘë¤oyhQ¤àqéIyëà¬FI¯ 銂+‘î»”€v†Ö' ö¾ï€½È³<10><>,yší,¡÷"<22>HmOÜÃ:ÅŽøybEU3ÃZeÏtõ“° 1 
€‰_TŒn[[È°µ:AYÕ¢ìõÙú'¤¿üÚ52½ˆæ¡ÊÇta³ñÁ¶Šk ±É9Ý£““ƒMÅ©þÂBHÁÆïôíçU탘9ÒT¤t¥B±±¶<3Ú[÷æáÅ®{þê«ÔŽÇÎfT·ÿ™œ<E284A2>oaa¼‹øœ¼àE¬gâ”x·èk‡:^íÖúêÂä`gõ«ûçBœMwµ†Xjì”t¯¤¦{dv‰}™U‰6¤²L´~Ê»êâ“Ï=c“„ñŸµ…gv¿I œŽ%¥}n˜\X©²¤1ÑÝX]˜BÒÅ÷<C385>´‰FF HŒÜ“ò¾ýŸœ@<15>÷~ÎŽuÞC Øz;<3B>ûõ;%?k Îzu­CÓÕ¯#hL„ýO¸q @ƒïï† pcfRèVÒÿâÌÄ8all´«2;fwV1w³ôŒHÍ.ùÒú}xrf~qiqa~fbø{KUѽ³!ÞÞ'.笩ê¿eÅ{Ò>0A$1´ô¿pÿeUSÏàØÔìÜ‚γ ô÷?×NØï&„Ñ•Û<1D>¾”õâýç†ÖŽî «écNê4A¬khíU\ÙÔ=06=7¿,ÌM<C38C> ö6U½z<C2BD>vÒ‡þ
÷ƒzŸ¼”SÆI<C386>…œŸ™îk®|q6ØÓ'òjá§Z¤¡¾þóKL÷ˆùtlß;29óeŠ4> R­ÎÏM<C38F>ô4Ug§žô±•ïIZdèy«¨²õS__Wþôêq;ÚÕ0OÜ|QQW߀Të>ßÁÒ¨ˆÎÑÞª\£=-¶‹/0U
j[p±[{žµa¦í„Ÿm<C5B8>µ555¸¿},¼*6VA$Á³lZæ³·ŸÚ:{z{{:Ûê?¿-ÌHŽô±3·öˆýèQ®¬gƒìÕµ'’è™Ø<1F>ºx7¿¬¢¦±½ !{»;[¾¼{þàÊé`7 õ¿Zh@¥æpòܵŒG…EÅ¥¥%%/ 3/†jÉ•ð#Ñ3sð¾x7¯´â[3·§«£¹¶¼4?=%:ÀÑLÿÈ e1s
<uùÁw_ÚXHÊÐëÂôóá^¶æÖ^'ÓÒsrrá¾wåôQ+-Ñ<><C391>Â;<1C>JNGÒ5M,éÞîöæÚ
$}1;eûéYùžº|ï!"}ôèaÆ¥OsaýDž¥wì¥ ø€—œûWOµ­èX^ûøsEÓæj=©î]àPqiƒ1&m`ãlª³²èqvÖü²2ï$GbìFþ´<C3BE>®déâ™t%ãÑÓ—%ee%E…<45>2®$…û:˜êË -\¢¢£O⊠ñuÐúÇv†ÃÑ÷Äé”Y¹ÏŠK°Æÿê=àf-—JD{ª —[8z„EÇŸ9w8—èbºåŠ<C3A5>!ï­¬¼çÅ¥ee¥Å/òÜN‰?áëla¸ëßwö^nåxòìÕ{<7B>Ÿ½DÈ—Oe\J ÷AYè?ðÃNžde‰ö²‘«Æ$5xÒ>a§Sn?@Ò¨ŒÒâçyÙ·RãOEÒZÿè%Ó/Ï 'kttÔ‰ w+m¢&€éZ{…“;%{Ì‹ý‹<15>¨VÊfdPA0ÆX4
<07> h Dôƒ‰°„¹ÞOÙ)qá'BýÝ,ô5œX±ÔÐÜÞÃ?,&áì…ää gbNxØšHÅb}cK;GGG; ´/ü«€Š30µqõ ‰8E!/^¼pætLX §ùÛ[iD©<>±…­“«»§7àéîdm²wؽšÚºøGÆ%<25>»p19ù|R\TÈQW[3ÃßÆ+@$‘É-<OÄ$œK¾ÈÊæçnk¦¯#èêXÙ9 $ŽŽööÖªÏîhÉÐÌÖõhH䩤óɈá\Ò©¨Ð£n¨ ~gPfVöˆñÚÛY™Â¦<Y\vV(|ˆÙ§èh`M%¨µ‰„À=à6Ôº¸"ÁHØóÈAÏÛ+ÃßrϹÚÙÛÙÛÛÙÙZ[k^N DWÏÈÜÆÑÕÓÛ­ãêhcn¤ÇJŽŽ/•ÉôØO&Õ• Ú‰tô Q¿Îî^>>Þ^.Ž6Hcßÿ*îÅ©o`hdllld$7<>í«<@ŒxM­ì]ˆ^|@/ÎöV¦†úÜ <0B>¾±¹<C2B1>“«—7räéêd«.DGªÇ CY褼ïr'“YÛ;»S>,i3úoX¸±DGD HÅû½<C3BB>¥¨=¸ã‡dYVŒ=ϾìÀÜ’ƒÚŠt¦â¸Ê eÔ<ì¢â³jÒ„[€rcªýeZ<65><5A>%ôdRdù8"T€TÏÐÈÄÄÄXNjÝí<C39D>=0e/D"ôr™¾¡ÜØ-+7@Ywý Db‰@süÆ7<HeD/ÈèEOʪð?r¤ƒñ<>†ú¨|í€dNOþˆ@ªg §LŒäì?Âaû{°èpýÌ‘ºÌ(vmAi7 õD¢·WYñ'<Bm ø!kH£[s}ŸÔ8#äá_ô€ýër
(AÛÂøÿ+@ :ùú…òóR"Pÿ·üKè;Ç>i <69>¹«@MFmZ<>0
&Qn)<29>¨XªÎMòÚõ¹ÒCüг<C390>ÊV}·€1s\ û’Ù¨\z$ µ,ˆš˜AÖD¶‡ëŸ¦ìýÔé!þHmŽ³/Æ©[<5B>4íqF\WµÀ,˜B7h¨<68>°ëiggsi¤±èJ¨½á®Qòèë0êïíkGP ½˜½ªgòA7‡`b\jìl¯ÏÖ]=á¤užã$füKph«ý@Kâ"@ÃÅšŸº?Ì <C38C>ÝÙÍé<>DÅÚô<C39A>êgWÂwÿ÷í!þˆŒè½^ú2µ—0ª/jbº÷.P˜ªÚ=¸ÐÓŽrsi¼³"7%ÔißÒÿ!þ Л@ùMìãÁ´:  áE™éH£[ ä÷`ž!*b+½éMæ™ ‡ßíÕâ¿=‡(úҧЦèãh<\ìæ
<EFBFBD> ŒRȠ̀¶%MVçG:«žÞˆõµ–´žxˆ?RëPúJ0AÐþ¸¸¦’`É @;;Û ƒÑîêWYç<59>»šïÛ9;ÄŸsÿËÚÿ'¬Ý’ àÜŽT<@ÀŒLÑÖp±Ø¡Ï&ÏŽt×”>LöµÃÈpH.ÄØyxÑ>§`mŒ •FfBknm)¶éðž´ €«BÐ`M½»¼09ÐQ]šs96ÀÉÜàpdø£!2tŠ}$j@<40>dê/Åúòüìì²êðÙiCx4 °VkK³ãÝ åE.<2E>
t±Ü{¶æèûõUüq¨Ÿ”ó£}<7D>=?ˆ襢m¥Ÿ  ~ƒŠÍ<C5A0>Õ•å…¹éñÁ¾¶ÚOÅ<4F>o_ˆï:Epˆ?„Ïÿíz{y´­¼¤¨ä]eMc[÷<>Áщ©é¹ÅÅåUz5p ØÜÚÜÜÜØX[[]YZœŸ<C593>žøÑÝÞTSñúùÃɧB}œ,þíiŽCü  ÿ¾zÚ:C+
Ðø¸4ØžÿQsåÂÅ+·³rŸ•¾«øZÛÔÚÑÓ÷s`hdtlbbrjzrbb|ltdh ÿ{og[s}uÕ§·%Ïó²n¥%<25> öq¶1ÙûL‡øS¡ïEG<0E><04>°óK1×Yr5ÊÏÇ?$"6)ùÊ­{Ù¹…E%¯ß—W~©®©­ohllh¨¯­©þRYþáméËgy9™w®_JNŠ >êîH§zþÍ™ŠCü9<C3BC>ZÑŽ´‡P<>(áå?s+;'woÿàQ±ñg/¤^¹q;ã~VöÃǹ¹<C2B9>ådgÝϸsãjÚÅs‰§¢ÃCƒü¼ù+ýÿüTÏ!þÐGßžÒG<C392>4Ì€“í<>}Mé“_úrc3+[{'Wo_ÿ àã'Â#£cbbc¢OF„Ÿ8|,ÀÏÇÓÍÙÁÎÊÂT}fì<66>þIëÛGÞÿ2L;ÓZ ГrùÇû+ü<>ˆØ7õ @æV66t<ÖÉÉÑ‘Î[[Y˜šÐé>=]:"yHÿ@Ð÷<C390>ÒJ»ø[§DôQ«ô0[íÂé8<C3A9>}P¦§§GŸ/ÔÓ“Étuu@ðφXî÷¸v2#€1éé;<>ON»ò#¨à°åÿ— Y_§þ‡ãÝØžï.Ió×þ\È!þw<C3BE>½‡ÄüÆIÕçÿ4Ø^úœåhp8ü"=Ûã·Þ_Ôþ„ ¦ô´³³9Ýþ2ÕßJ[R8Äÿ,tL<“òêÇùç‹šE@Ùù¥\ú’ï±û<C2B1>ÑCü<43>B¤grýMÿ»´¿p3(7g{Þ§Ÿt7;$…ÿ 1r<31>{X=´.n5v¶×&ÚKoG{X
ÊÿWÀ 0â
LRŠZ&×°ÝŒÆ<01> Û+c-e Næ†2Z8<"H¢#5<>êýî%ÞCü 1p ¿ý®{Ž}D w¶VÆÚ>ä¤<C3A4>ôw±132<33>Ñ""€U$P(@,¡÷´õ MÌm]]útÉ!þ1é˜xÄ=¨ú¹ >¼„É<03>A
“½ÕÅYi§Ãü<œ°±`fl$740Ð7Ð×744Äê³…µ<E280A6>£O`XL\ìq=_&<Ä?
b™¥ï¹¼š¡%µ=öž € HÇ,B±67ÜñµôÉÝKgcÃC<02>z{º»¹º8;9;»¸ºyx ˆI¼xíޣܬKaNÂw˜ñÏ„ÄÀ68íyý°úë° FtÑ'Eg‡»*_¿Èͼs=-ùü™Ä„¸S±§NÅÅ'ž½<C5BE>vývæã§%¿Ô×~x”è¥úÚ!þ™Ð‘;„]yÙ8¼DM¦Ö'<27> + “ƒ}í ß*?¾)).zVX<56>÷$//¿ðyQÉ<C389>ÚºŽŒ·²ÿÎEt‡øÇB¤kâ|âÚúþyõ_X0`úŽr{cuinjl¸ÿ{OWG{[KsScssk[gw_ÿÐØäÌüòÚÆâ÷W…MìCüc!.Òßnqaa @>”ŠÍ<C5A0>õUúXæÜìÌôÔÔÔÌììüâÒÊÚƦbŒcuàÓ<C3A0> CJø§C$5v
½ø¸¼clq]ø 6(GR™Z\¹­Pl)èÞRloÓ©w8Àqu¨üV°å!%üÓ!Ò5²HºWZ÷cjyS<79>€GF"ôßo⟠±Ž¡µOôÕ¼<C395>­ýSKë[$/ ™qPqqØf ž•+ýå7ƒ-)áŸúP°KpÒ<70>g-?ÆçV6¶0<C2B6>Ðf47ˆ­Î€'• »–Û[«“í%—üå„ÿ ˆÙ?¶ŸOú¡®s`|viuC± r@ƒã¢'pUÍ`»½½¹¶8=Ôñ1[óuõCü³¶`jïsâÌ<C3A2>GÅåuí}C³ +ë›[L,D“kÀÌ<C380>·é/ 秆zZ>¿Ê<d{xÀé"±Žž©<C5BE>WplòíœçoªêÛzûG&fæÙTf
¢€­­ÍõÕå…™Éá]<5D> î¥EûÙzüŸѱ•“OHô™Ëwsž•~üZßÚõ}`xtbjfnnaqqq ÷üüìôäøÈà<C388>îÖú/ŠsÓ/%†û9[Êÿ“¯ZâŸHG×ÀÄÊÑÃÿøÉÄ×3¼|󩪺®©¥½³«þp¢§§«³½¥¡¶ºüÝ«§<C2AB>2®^8æçfKG9ÂÿD"‰T_nnãèîv2á|Úõ»÷³<+*~U”ÒÞCþã7/''D‡z¹ØšÉõùÁÿ(0LèêËM,¬\<<3C> =—xæì… Ø<>Œ ;æïíæhkalðûM‡ø߀X¢#Õ302µ ÷c]Ü=<éÿ&||¼¼<Ýœ<C39D>ìl,ÍŒé/ þÿÿ~þ!þDÐ5]]½kddlbjbbblldh ¯'Õ+8äÿ¯A ÝÅLe<Ä?ÿ ›ðü
endstream
endobj
6 0 obj
21412
endobj
7 0 obj
<< /Filter [ /FlateDecode ]
/ColorSpace /DeviceRGB
/Width 522
/Length 8 0 R
/Height 134
/SMask 5 0 R
/BitsPerComponent 8
/Subtype /Image
/Type /XObject
>>
stream
xíÝÍŽãF†aÍL<C38D>»ÝvÛmc
/´ðÊÛ¹ÞÄ\<5C>/½çE¾¨§O)J©,g•ãY|
)"õ“e_.Ÿ«ßÿ<7F>º,˲üüö€Ÿþ™ªÿùŸÿ¡þøã<C3B8>††~jõÓO?Qù媶m»\.ÿû¿ÿK]weYwåÿþïÿrñLJ~øáÂè믿¦Æ_ÿúW*¾zA4—eù¼üë_ÿ¢ÞT¯Ë—Á÷ç¾W¯¾ýö[*þö·¿QÁEžªÿþïÿ¦ø¯ÿú/jC'ùË_Z<5F>:37rúî»ïþùÏýýï§6ÿøÇ?ÖËó °½|<7C>S¿ÒW㊫7ùÿüÏÿ´‚ 2HÀ4'ˆiAˆºêzãÏÆñ‰,Ëò|“@åÊO=À˳¾<C2B3>ßp.ï™ÄEÕOÞ8²Tµë³r²ÈSÓ‡ì´FkŽ|
q½^/˲¼ ®¨¡ÊÛ¹ëõzò+©å<C2A9>åÝ_ß|ó<>_åŽP±—bëMÿñÿA½)ÃjA5³j`°6cgžNSï‰Ë²Üeï
Ï 0hŠìÅÄ+Ìò¹à“B;dLJ8õX®ê@Y5WéO¨¦<C2A8>šnO„eYž.
à °½Bí\>wÇǃ.²jžº9à$ç±Fšc8x"œÌ㧉½7BËg-‡5ay¢Ÿ~ú‰7¼ ÈÇêëqﯗw…¤G¶;ØÌ•–Be<42>uÏø(="GkŽ2€U5<07> T</ü(ÁÓ§jÛ¶õEÓÉ?§\¿„><3E>7\îT^VðG¡a€¯;Ô/´—÷ãÇü¾@˜šÖ)GRA¨ÒC!lŽµ¢Gä°i<C2B0>ÖDí©y<C2A9>7Gqñ„_¾ ß}÷uyk¼ˆ¸òâÌKoùLyp­Uë±i…Áº‡G+zT3ZS­³5žµæžï¿ÿþ×_%¬{Äg¤¬ö×Ë_½0P—7âÝd^k  …Çb}vx'êËG~¾Ëñq4©"£†8hš­ <0A>J&xl<00> M¬{`yçÚY]ÿó,^³}ySí¥Ä %s,8:ÿ|ù‡HË»ÒþøÿÕ¬ô`°ŠŒ„H<E2809E>ÁÚŒ<C39A>­¬Í´óÀõz½,Ÿ¾)=snû ùòD|MM=P_zÜA¼;<3B>—w¯Ó©{<|Öý Lù<4C>µ¡@¨Æ¤“B5ö <20>*Hˆ±gùpñ¡6\”¨Ë'Ö^kë(¼7¿ÿþ»·Þ5ñÝ !ÇÈcgU²Á:ÅC TöPEnì´Fš ý•=Ô“êàä@PvN°©Ë{ƉíM¡>3šŽÿSÀËÃò净
÷6<1A>ÂòøW|_ýµºÉky© À@ù¤ 6Xï5.U{È0P£5AHÀ4¬¨ûîÀåÝòÄŽGÈSøöiý¥ý§çQ¨ÖïïÄô¿—8â6­7eê”Õ
jPÍA'­ÕØ3jc¸«ÞüZuùÃyå·ƒj^ž®þk¸z¬ ŒÖÝáçŸæ8p ©5•=V%D®èa4ö§'aÔš6© à 9á@v”ïK¯×ëú~齩G$Ç«ñXS^4—§ãî@eƒö9j@ź;¼|”¦Ê£“ƒ%Tª›=4Ah준„ †Ô 9Ò$À@=vsŒ;ªÉW¬Ë{pæî€z¬½;¬ßÞGu‡×Ü|ÿý÷õ.ïJ;p6­£ô'Œ|ÈŠÙ´6­“&HPkÊN+ V¬ª­Ùx=Áú~é³p|4—7âg‡åsñÛo¿åÿÇÁ ¾p¨"GkjÚY9ÀZM{@@Â]X
@¨Ò“0U5[?/Ÿã§˜§oóÞ;O(8ÿAæ£4Ÿ>ý¿ÒÍÖ&|Úau'Oq¨È;®å<C2AE>ÂOET<45>9d 0[<5B>eŒ¡Vœ Sã£éIí=D?'1jpU¡þ!¶m» ~þùgªüêwråÙ>nmþ£9íá­ íÚòDõØÕü äIyP<79>#•ýp½^/ï[ B¶VÛÇúÇë {„fÚù,ÞîÝá×—}ËƃðÙ¹¾l?<
£œ``Y^}Ó2׊1`šb¯ÇZÙcE6[a”þ1c{¯½ÛñT|Sí:æ?%çÍCß^Åœ¿œmÛ6^AÔË`uÞeû÷Ú_ÊM¿|ðfq†Oaû¸Ší%ä$‡‰Êá š ÀGåRÛË ì *¶—&\Ñ+ù=¤+ªÜ _üß ·žWâÛo¿Í¶=íPrŽQ+ŸWÅ>§Vôp?üàó}æþæoØ`B°m €Î0<C38E>γäxµ§“SKÛǬj'G<E280BA>l6<ì4*¶—Ù²êOƒ#ˆ<04>#Í„ã˜ÖcÓ4AˆÚ¬9ì´Š ÂM ¡²'•óÜ üû¥O&ÿÕ ¾ ùꫯί= <72>àŸ®a¼ËpBrM èüå:Ú‹âǯV^¾À«ƒ &€ÍfÏžèz½¶õÃÆmóo>ÃÊ‘¥ÞgmÀ¼£`7*·3l*Ú¦F{ÎK†ZÕ^JT½þÌ™bƒ© À@ÏZñÒÈÙõÎOêkä”[KE à¤Ê-ƒ—ê³N T$ 9A4A5 áaÌžú蘭ÍØÙzŽM=¥ÛkÄËï³äõR_³97šºUܼ¨÷_,w©Ï='mÝZ·°VÔMÍà<ßì<C39F>Š¨ûŸÅ™-3` Œ§¢>÷ÌÉ;Oê]¶m»¼Ï¢ņÁ}Ù-SõQ¶×ëõò‘{ø“©«mÕI9i_/WøœKð°ZÓ_ß¡å´ÏögW³3A ^ìÿéKf/%j5^úè¡j=ÇÍhýiŽaj|Ôë<>6 Í„ŠN^ƒcDýô\/ªjgç<E28098>g>o©ÇX„*ÎaÏØzå?ðý÷ßóAž¥Xi<58>§©ÛYóIíR†¶®:çx Ç¡â)PïÕ>ødÚlRÂÔø(ï©ùø~ŒZ±÷Ú€q·¿oѽ Ö=ÃJ©ÛCê1w¸X+"œÄ`>}pk`ïS9+ÀHê]ÆgÑžøÉ<»ñ¥õxÕ¬Úc¶Fk>†I@¨jj“ÎÑAf«ÌÖ¦u¶&ìÉ1âXƒP4ç õ15j“Õ5nŒõ$Ï[·{3Ÿ—³Ñ=ÀÌ 83{ø~uïŽæ*Ž±[@˜ª+­Yuþ“7Ç°y Tucxá{¼¸æPŸeÛ¶ËËÝœÚÔµcÜ<{2ŒJUÍ<55>S]
¼<EFBFBD> >Æ·=«±êTd3^ÏW=o<><dÎLáÀÞ`#APÍ7ç„Û³‡ï-yïç¦FVa°8ࣵVôˆ\¥Ç`­èA5ÃfªÈSí!š h/7{yŒ¬O±mÛå£8ç·fKÔš• r¹ãEÄœTšSŽ¬öÎÛ»ÔmK6ŒUl-õظµrjÐŒ4 ÖÇ—eo~Ùîͱb 1®"=kÅR ¨²ãWý1­o?êÌu<C38C>͸…²ÿ`A9ì)ÜáàÊF<C38A>ƒmpíÔ 95Nâ}¥:m²ÁóÜj<C39C>62öÆp{¢rñçXƒ|Œõ ½i°ˆÌÔm@kÊNk´fÐ/s* Èl=0¨=d¨0X°gïÏÏpó&¼ñØÕ•Ö7;= ­US<>#÷pÓ¡Ê­µµÔfÍU6À<36>À\Öܱ\ÓÚZ2CBE'ªù-ìm[e§5Z3Ø<ew²»"ÉÇÔoéU·™Í€<C38D>
ƒ {Ø`jŒãíq¥¾¹ùŒx<C592>PŇJjãœMí4[«±GnؽÆ7 jkñ¹«æ*¨¨!2CÛóç9!5h¦x(lR£5G0¥™€ä„ ÕÜLJgÈ È\+ Dk>Ì7*œ“\
Êq¯N®±«ó$3„=^nªçd&LPmA˜â¡laµw÷<‰i­AÔpF½X¡î<C2A1>©Ì<¥™€äq‡Ü¼œË×™Y2×
Ñš7e|]õ]NžY¦™Âž“+ªxýRÑž]]¹=Zñ(Í´ó)ؘ{'g<0PAa ŽöúGu¤Ù­©ÚI¡J<C2A1><4A>
u<0F>Â`•™º‡o$¾šq°¨r-µ"¡šv~Ää 쩃\¯W?Ý{<7B>aNX†ÔQúÇ€šåçeåîÉ0DaD?çñ¼.Û¶ÕßDxî ì­TGš<47>9ü²ý^|ò¢Â'Åœ ¸Š=<
ƒUfê&Ï€ÑA¹œð^œß 94Ô¦ÎYÑ‚È ì9~ô€÷kžŽÇ îÍ–~‡!=¨Yµ'™˜
„×`BE¹¢At܃NÎB´æ<C2B4>6²5á¹<C3A1>>lÛv¹Óx|ÇU4 aÏøhí!ƒÐä‰àB=n02!AfêÃê÷ u¥L 0Fd¸<C2AE>zÜÃjC'HÀ½9¸AÔ{Ó½Ø`©6²5™yÕ|o ˜„=㣵‡ B“'v¸ßéÝÜeYf™ú°ºxË ë½‰gDu|WkâfM0ªÌVÕ Ví峎Z?MFµßl<C39F>ÖÄد'Õ¿ñx<C3B1>áz­ÕØsg3nÞ¦ÏŃfÔfrB´6ýŽ·6vÖ­‰±çæÓ1 T™­ª6­ÚËûõã?<3F>»„Q]<5D>Ù­‰±ÓÎ=ÿüøXîâ*¬ÕØs§‚ OuÞïQ<C3AF>ùÙ<C3B9>Ý šQgKNˆÖCýµ6¼s£ú}õÚFVÎYë±6¦5£ö“A8ÐЄ<C390>Š1 f´æTÆ$`/OMÐ Øóþ>µmÛeGÞ‡pg<¡ÊTFôÃ@mj§¹V¬{Þ<g3Cör¤“Â^qÔ{1-S>Të]ö6©NEá@@¯WwÉñª×„:gÍSÓt` Š ØÀÝÁ¯e¦~ùå—ñgt1 Ô¦všk…Áº')ÿ.b<>Ÿ/Tœ®"<22>Ü­Š¯¸ ǘ„©ú<C2A9>Ù:¢„†N& ÔªöŒ™*rS;É0PÚYsµ×u€Ù
õÒqpæ<70>¦ÓŽ<C393>µ§fÑBE<42>È{Ú£g.A<Yj]°f<C2B0>=M$ìᄧª6SAhèaª>döI<C3B6>~ûí·úï[ƒ¥@š0P«Ú3fªÈMíäsW°IÔëlSu€Ù
S<j~Œo´ª:gÍ¢„Š÷´G[ó$ µº9O$ì©ÆL¡É¯N1}igY@®ì±Â`} fYf*˜+z@8PÔÓÎ=Ó³wy ?8ì}§4N8öœ4]°vA8ƒ;ÝôjSg¨¹ªO­©,k°ÂW\½JLGI¨ÒI@È•kÌG'<27>W~}+Yf*˜+vH}¿:Uß=NwÚ8m5>:N2Ž¹Ùã$îä3Î<33>lÚzU;É œÁ¿ù‹ÏÞ¦¶µ´¦Òéþ±Â~«j®ÒŸedLªÒ™Ð¤ŸªtŽaäCVA* TÐ5ÒL{¨ Œöúƒ0¤VÙç'åtb*¨Uëa0Pp€IDÖ˜©uª\µmÛåå5BÍ0IE2ì^uA¾á­²¹DΫX¯ÈfB•Î„&—Vn|ÅAÀÞÚ©ÃȧfƒA* T<10>eý±žÍ£ŽÆK™3PAíõ«†<C2AB>:Ž?î!ƒ|³D=溂` “ˆ¬º”™
j†o¹s&T9íôh3œW·“,2‚ÐÐ)r¸ÁðíÍM,¨"ƒP¥Ç`E½¦ ÖΚQ5£6É M` Š Më¤ Â<> È¡in©Ì Ö„çISOË6 Í<C2A0>fZs´mÛå#V<>ºÈ˜@ wˆµnm¸T•a|ùœk5<6B>ÎLA5ïa d0ª¸y¬ÒãHçA½œ§©³ÕŒÚl˶“ª5 œ„*2 4YJìy¥™pRÛLg¨<67>m½f]­9⬠ã\EÐf­<66>KUÙÛ<X5¨æœí®Ep<žGa "¡I?Ad$àÞ\<5C>ýô€ÐÐ BØ´ªæ8ßYeÀñ¹´'ïñ°Âù½Øªfepœœ?§eû‡\¥©bä8=V0†
~|§ÖÅ•Q{ÆgŸxU ·Áz^žø<14>º®LФŸñ`A2êÆ×̪²,j®êøcŒ!œÓªš#Û\M<>H•©dÓú™Á`U{š¨{XÓ'U9¡†Ì|óé;Æ»8 Ö*#ƒ<>ÉTÕã$cœy:CdAC­ª¹Jÿ¦xTä  C­ TcOss@Ãx`°Š,r3í<Àx¨òÐŒê›:O$Ô÷ìMؾü™bYWam¦<6D>ò™.h§•õRï Ù3¿ÓRorK`°ÖÕ<C396>å<1B>mÛ./öFŠ™EVäsqkýôÁWd T;Ðf¾‰<C2BE>lµÎ@¹¹¹% “ƒPg«¹ªýYQ¾9P¬öú+ö6¾EiÜøcu-Ç×[Ÿ”sºíÔʹŽ#utsNåhÜB<C39C>œêæ°: ™Bc§5Z³ò!* TŒá^uA2 Ô“öïõWíí7Ž¯ÎYk=”SÛ¶]Šé-é¬2[•ìIÎ<ãÆß”Iy¨ª÷GðZã¥Áê@ÓªúQ]lLý«B_ìV&¡ÖU7£æ^CLç'€Ð8¸½<§ßZ+O„Ì9†{ÕÉ0POÚ¶í2ÓžÝTV”àÜã?ÐpfGfÁ= ïÂ<C3AF>{¾^x³Ã©ÊŠ TP‡GfÔ ³¼
üXísßÃÈúqª±Š™©ªÓfçÔSu6´¦Æα禺HÍhMŒ= †T$TÓÎÓñµÓlEžºç§<76>¦:¬ÎV3Z3¸NRïÅl"WéIسw_« Ö\l³—˜þá¨üó<Îöëõzùø<C3B8>¶?ö´?Áj³©<C2B3>ÁÍiG×ëõ²c<=| +…!žHM½ž1]ã¶m—ÛþCÊæ¡æp<C3A6>\©õ³CÍpØh:çtWT¼3áüáä!WYËtZ¤?#¦¥joL&Õ·OS9I|Ž¬ÂÙ¬aÓ
†Q+>‰{wð¡ŒDÍM{Ȧ@ ¨Yô€PÙS«ÌTT3h ½Üð³5j“ ÂÀ@EÂ_‘ñ¨•gŽ¸˜sáÝ{¿±Ç3‡©Û¶]
&¤ŽŽ'|/dêØx7¦VÕ|ŽWö!“€€Ô,ºùª<C3B9>ëõzyy_Wt“&â™Rµ÷ýV—3¨qï<71>\ž*™Çid¤êaÛ¶ËË®£VmÎÖ¬¦·øf{YË'¯5ÚG}
o­žŠÔ†NÜÔz:<3A>»µÎBLT*z@™©0PA ÷Ê‚*2[£5U;“ TîÂ" É <1C>vhWS²HŒ=â-Äc—ͬŽ™EFÙrêM Ë" ƒ€Æ€ ®$Ôæz½^žÁçèê²Âg°«ó>“ÉëÎI…<49>
Æ€Ádœ¹RMe ³5®×ëeÀÆP•ñ*OÁ)MEÖÈä AŒ<41>·2[Ã<0P_¯Þ¾³I''çÜà !ˆ/BsS«S<C2AB> šãäíؽä÷¨u~ªÌµKo9Ç2^µi¦ š+úe¦ÆqS­³5cÚß:mZÖIa ªæ»Œ Ö<1E>¦<—jê`µ.{/ÔóJf꨾v°mÛåß± šfÖžhFü•n¾²=º9~óÈL ž&5êO$÷jûJmu6ÛJÕž#Aªre»×ø…a<E280A6>6™5ê¿ Õø¡{xãqïç ¿·ï½×—k±"!|EGÝíLš1=pw©Ö<Å“¥ÆɵߜöÀñ²yÔ`EB÷˜­ÇÂhì·ÇZÑÆ0uüè¤Â@} w½ösæŒpÿ½0oH¨ØÛ`úa *Ó6~[Å̼#´ÓøauÕoäxÛ¶]U¯W®ÅzŒ·— ŒÆÅíÙ;(w¹^¯—[\]ãi™mnÛ©:<3A>Ï<>ÔËòöŠ^e 0g<30>Tdˆ9Þªš+¾ >€µP÷LåUm Ö©ƒ‡Ä:ƒæžúè4¬qWS­³51öàfgrÂãÑ¡ŠÌ£ ão¾ñÊù¹¤ƒù©ÑšÇx“FU¶PÌÔF"/1ù<02>½×ÝMuEmhÖGýöŒ¶©#fáõ¶m»|”­urk´fûéaºÍmÌg®Ï£¬zŽUm½šn¡xˆm,ˆÞI¯×ëåQ¼¾¨{Úª³UÈ7;¶‡€¶Tk"#_cœ¶öÔM•w‡¶êºÈ”¨ <€AˆÖ¬êCf«È"7Ç<37> ¨yOSsÔΚњo­<6F><EFBFBD>ñ¬¨çÌø,ꣿ7¨<N2íÁETóëñì˜d×­ùX/¯Ç7$|»2½ÒNŸOœŠú.4<EFBFBD>êóÙ<C3B3>úF¦}—3lÛv¹ßx7¬û¬$ÄÞ«£M<E28093>*î&Ô{Õ¥êld™©1ýiƒ<69>lÃZcO•G@á.Y„ µÂ` š"ƒ 3õÀÞû­UëI3a”‡nª#ÍT¨Ï²ýû ‡³zs¾%_FnN[¸%#>Gðv.+_Â'±.bo<62>{òÝZ]<5D>9Ax#~NlûŸ5Š È7où¤¦·¡{å:yscb¹m;||{Ïâ<C38F>´Õ¥éNÛ3Ýò*ó€óœú6Æíáæb'<27>R™„H3aO@<10> š €ÆPÑ ƒ5hÂ@¹Ig Á†ZApÏïáè´,B„6à5ø&?ߢ0-\<>:jj¦¢6Îi•Ù
ƒõ¹Üf†Á
BäÙÝüDÆMŠ*&<10> ÞöóæŸð\$¹;°<>>#¸ªÈM¾ñKØÃâ 09«  ~îxLÞ 2-{x?BñÄ m€Œ„ ¸ù|<1F>µÀL×ëeà†Õf+<1C>=^N×½e]WÄí`üéä"S<Bcg­
ƒcz@PÍHs@ˆƒfÍ¢Œ!ì±îñQk´¦‡¦¾=à=!'?<ÚÆcìÑë_¹àªUß%¶uµfN¿¼'Üáb6šÖÙš<C399>É +Û¼ç<C2BC>Õywp涸M+Ü u¿’I83XÑšõ®”¥‚Á Á§öy"Wa°yãªy<C2AA>PG, 0õY<Ðá*@aON¿Šm.Õ:ëÅá1LNb#©Ú[v진hÍ „Øk¬' ÎC«È 4t6© ¡ª<C2A1>5ŸTO€ëõzy¹€Œ¯6sk¾<6B>éŠêi³mÛåßñr¸yË`Z@ 7ÕmÀõzåá.uu5ßk<^bN"—ô×ã-u*+5Ô}Å ‚·„†‘ „KñÔVôzu¥fª«>ÀVQ0 îð½ÛÊcêæeE0ó(à`@@³OÈfP•ÏÁ£.n$Äɪô¬"ËL<C38B>Ú4×ZÙc­ZOkbì©xTfêT{(MC­ €ÀΧÞÄ`¨"»¸'ÿSL/ÁAp½7Mo.ë<&„ªö$'p•àfJ8É æ<><C3A6>
€€ö^w*Ÿõ‚©@¨ÒãñzåÅj۶˿s~kÕz¸5P«6 aW°‡½;lÃJï{7Çi Ç«¾<>/\lÕtÖŒWîpüãã´ÐõÞÄÓ¤6îÉ6C¶™B5þwZŽùGG{Úül {P¿Yr#UÇ“E>ps@´4A¨ì±Fmš­"ƒ€ƒP<C692>ïTJˆ±'x„†NîrfÏØןüSn@­â¤¢ÞÄ÷]¹ÀÖËÞâ®Â
0Pa ž”ë\u]¼e€üLH ^‰u?<Œ;/¿;<10>­Íºªéz³ì(“°ÍÔ׫Ga*kŒ3«v©ZGüîp½^/¯“Ž¶ßêzkf½þc:Ô\[<\Ö
YýbἶT&„Ùª¶“¹¹xwÈFÖÁS7ŒXDdÕ¼§Ž©­ zD` Â@Ad<10>PÑ)òœÔ>PÞ«€<C2AB>ÚxRÕ—ÌžÏnT×h¶6cg^ŒMnRà´wA+žˆ9Eë¥>o 8
u<EFBFBD> Tx¦±êLJ,ŒDͬúF\Q­SíÎâ'óÙå‰SŸÈ­µŽXµë_´RïÂâTÝõòÉŽbÿÔIêvoâî@eq* B“΄Y:Ahì¤71 „fì¬={9ì´N<tÓ¸lzrtöd¤lRÅâ Gœ„Á>ZÖ“¬„jìá},õX}mæUsÌYdÀÁ´Ù3žóî´,H¡ioÀ¦|î\¬¨ sи<C390>¼ä³U¯7®¨öø|5ýŒé`ë”Ûü,¬QYñ†„ÎC</У›³åÊùu<>r½µ2†Í#L<T1 õÌIxÀIÜ*jÛÓ^ø “Š Kå¡„f¯?`<60>ÖDí©¹ÉC5TôÄqStƒFö[<5B>MÇxìÀµzˆÜpÇÊÉ<C38A>lU´5f/R^ª„{q6²,<2C>æAm<41>£ƒ×ëu¼ðæ_Fd-™a ðYpò¬ý•¦O§¢'2F×ëõ2`Å_w<18>²Š„<E280BA>ÿÞ¡a¢5_ƒ÷NÔ`‡PÁ*Ðõ&f<00>ñ¨A>ú.ÜSÚ ™ÉÇ<C389>íižI@ ØË°i<C2B0>ã¦èA5#Í„êL'M*zD*0PEáŒ3#¹xzÈêI²· #½Ò~{ÛpF»>ðc.ç0íâSŸ5bžé믹ožYãEUÛ$ÖxýñâR*Ú¾R[<5B>ÚJiræ*çÏH. í8ÞcD=6®÷ƒA@B88ßøÜôÊ<C3B4>?ØžqÕ »ŽlXIPkž|ë¨ééÚ&<‰-¤fÙ„<0F>ð˜,k°†M*ö*SãCӨњ¢Sfªj®ÒŸô€ð¡á7½*¾…º\a¦oäFž´Û¶]^~ª¦6^`y"Ôék0ë5PA@½\Њ„§Ë̾â|šðâöÓO?î¬+\iÅÍÅÞc8"krý†ð¬”©óDjØlª±ç)Ø
¨b<Ù~ü×â]=uºœ¡Ö§pKöøh¶Ç}[7l ÈA„&<26> ¦cì´î9x´>TsL;<3B>Mi<>4Aˆ4šôj=àË¿a)<”àÝõ5˜ „©ºê¬ôØôq}ÛÛT^SÔªmOÝØlcFíWiª REœ|vÚœbfêÓ×<E2809C>9<EFBFBD>dºÒcnRÓæaÌëïhÔ´õŠk¬.µ_ÕYd$¨5ùøp<C3B8>}ëx«£ªMÞšQ·<˜„Ñ8Ï]ŸöÔiÍV6ƒ©½èhÒI[Ü0jLÖQëoMÐ#²jVzªiç,(3Ud¨7å ÔS· 7Åù@<40>òY¤7<>Ïþ2{F}¦#?SðÖÅ”êUqSƒmù$ïl³S±,aÓú\mN7 öîS٘dÕ,ÆP1}çŸGÿ(uƒk~ú†œä¾ù‡Þ×Ø$à¦RA¨8ͨ_ÿùÎmêç¡<C2A1>=¬ÎàœTÔ>Md#]¼ Së´i<C2B4>ª%@PͨÍ95ƒ&Uz•=TAЙ,{¬ªyÄ£ á..E­èÑÞ y†mOÔiÉ ¨æØ»;lÛvÙw¼©\̽GTY{ Vä)4¼ãg\Ÿ&2Ì­<>ûó¼zåÉ„T3¼à¨})=â)PUï`Zªôðdy.,ûx{‚²j®cäTç±ò|ëÌA§ÈǶmûî»ï˜<C3AF><CB9C>§)´îÉœÇÇ–ú%Þ¸ÐVš1ìpêž âQ^<5E>Í šHpu7¢ÝÀxÔ 3° M: 0P«±<74>Z¥ÇP«jnÚCµ¹—cÚygl®Z{ÚÌ6­ò8r”¹NªŸÐÅ<™v SãK€Ïï¿ÿÞ®<Mjã b¼02Þ x^Ô»Œo¹;PÅÓ¤"O<>C<45>gDxÐd-}~ lÛv9<76>q†qòôÆZ¨7¹HÔ¦óÈÝò¬êTÞÖŠÃA­ÏÈe© œ´mÛe_^ÓS7êÍì(Pݼ=Ž·b ®ú<C2AE>o²v6†ŠÌ‰ä<Ê­–.õ•2•ÅG<Âk“Nƒ*= ¨f´&ÒCá&†<>P¥'5¿©\ˆ878ÍQ·¡æŠ«¼ýö[ _MÚf¯×S{“<>Æ»Æ3˜„Š¯ýFºÉ BãqrSy‰ñ&<26>]J>˜M®/./êŽÕ¸'#ÛiÈ7‰ëm^žÁ÷H¦†óTé!€pDMÀ8ì¹XWÐktNqæPGYœUÓ ÙÏÔmÛ.µûÑ<C3BB>LNìUjÃÙ·åÁ" 4<wê±™©7eêøš+û© á.Ó¥ÒÙB*UzFyÈ`Y䊔fB´šîüé‰T9¬bYjÃ0ª½W×"ªêR™9a”‡ÆZîk¼ççÝ ÷©Œ©kiܘé="öÏÆÀ1ìO'<#' æŠ› •Éyv×Ëý¦ó³ÙTä<54>yÛeuTî ìLB•yøñ¤Þs¹ïðD¦ÓŠ,³Ì™ÃÊQ¦FÞvºñKQƒÙ` "¡á¹Sobqª±GÙ~d‡ƒBÅÁJ3ghæ9²8;¼}|Þé¯Çk<C387>·…„3êS•ƒ(6Lfê³d¶Ñ„<C391>*2HÕ‡j¾Ë¸àØ>d…Á:ªýd&È¡9‰Ahèù朜^|<7C>ÔñÎpÃêRS¬…ªé`;­×i^¤UÌ6PQC<70>dY™©ÏÙD„º%‘Ü#<FSu'yŠ:mãZ¬p¤uT„`<10>PM;<3B>±
6ä3 ¢f‡ã`ŸW¬¾8à6´yÒLx ŸàTŸµúÑÕ›~ÝH«È=£½þ© 6XÃ&„ )20†jì´§Öªõ´¦ì´"A6­¸^¯—<C2AF>g¬‡é¼½ñœVXÏàYˆ\±yõ]býBioò:Iãë” E¶uX8„ªn|Ïœ»\j¬"ƒõŠž‘y³Qa°†« )¤3!˜Œ¡â“µâM&Õ}ÂZ¨U¤5åödÙù¨õ.®Ë<05>“Ÿ†©ç9ÃÈ~f†<66>z Š\Ñ‚ê„ã×aªê²fªÈ™<C388>/E©çå³CfpB«Ìp'<27>_¦ù(#Ah¦<68>'eYȪYcOU%ƒÐÐ Cê¨ö×,{Ƶi¦Â@UÍwñ0!3$sÁ±Þ…uÁ<75>
FÍ'Õ­uq+ÆPmÛv9Áe© 4ÓΓ²åL"²jÖØSe*0„†N@È"Üa}Ë—ÞÔ= £ŠÅ©•=c<>¬f* Tµ¥F šqïÝ!œg¬waRAõiî9>¸ÜÅ7ÿ`ÃØH<C398>G<Jå…\ïY¨ 4éLˆô¬ <0A> ŒèA5<41>Ú£­©ÚYse­0X:A@Âȇ¬2 á¼ñHQa ªæ©Ìƒ 6PA¨è¡ª“È“ßkç<03> VÕÜ° µ:ÜäÄÕLV™ 4éLˆöšòÇ͆¥@ÑjµG[Sµ3Oð€à]~ºs€„ËZd†„ÑÁC ;ÜcZ¯¢7¯Ýrš Tô€P<E282AC>=ÎàédžQGõWiÇO<C387>“Ÿ—cZ9¡UÉÙ >;ø†¯bäQƃ0Uª9¦<39>{Ú`š ŒèaŠ‡@ÑB¤i¨õ FªùŒ:žÛ3j<33>¿KŽê$Õ^Ó†ÑAdª±GÛ¶]^üüóÏn-wêM™p ÍÉ~v&õÀô´§‚ÂT}(Ë>¬Îš ŒèaŠ‡@q¼æÛ>»X € šÏ¨ãÉ ` h·àê²Ú0š ˆ B5öˆóv¸?™M/Ñ£L8†×ã`«@ŽÌŸ<C38C>ӻñLRÑ `®õXCj¤IᤃÁy(¡¢„°iUrǾ ©·<C2A9>½EX/a³V&bìi² ¼¨SŒ¡bfªô$è¸<79>¶Çm·mjºR:³xý<78>Ã_7üã+×rŒy¨Aj¤Iá¤3¯q·¹a- „M«Ôš B´&¸ÄQý€pŒîàp¶ZAšHˆ±§É€„“øì0Þì2IrŽÝßê£MkÓ:å Ž´6­3Í„3Ú`T<10> V‚æ]\Ä
ƒUd;­aÓªš#<23>*õPNq j'UAtP<E2809A>=J?„»LOÝmÛ.ñRe ÈUÖ•PÕΚƒN¨È*øàpæz<>NemZgšYém°“PA@lZAšw¹ùG•Ü~ÿýw>ô‘Wg é4PA„H3¡áî<C3A1>·µ8Ú‘ÅÞœé'€p—¶«¦òöz,/ävV¨N;ð˜:-ZÓ<5A>Vÿf >#1^äÑq¿uOMØÓÐ T%@¨è¡j=­‰±'x„ÑØO ©aÓú<00>£§M<C2A7>„\Ñ­ zÂ&5Ò$€4ApÄ<>úfa4ö·ž4Ý÷Ì?^þªÿ$³‰<Úëçý*õø
ì†aohh†Mª T¼®o¾ßüa «a4öÓCjØ´¸9 ùõ×_¹2ó¬yîÙ᧢'lR#MAªÚêîÂ< ŒêÉùFꪹg±½û—‡ìIª­9jZSµÓ\ë1Æ€ rØ´0ªýf«j®è‡<C3A8>ªšAêmaÊ“aÊ¥¬`¤¯‹»î.n=à€ZE *[Â…ÚŸ<Žù¥ªËZ3mÂM¬”zž+ŠéŠj§¹ÖcÌ_7‰×8
'¹gä<LHÕÞÐU5ƒ& Ô=>j­¸pQŸ…ùA˜ºù<C2BA>켊ž|[Â7KTwï×âüž«÷ªK9̵ºq'û¼à0kc§ ¨9Z'ÍŠ@<40>ÙZ<C399>=²Ÿ
Ud¬ Té1XAyTûÍÖhMÐUdàq¹—AÏ®,ž Q³öz¬•Zá€ZOòHdÂÆ9­{|ÔzÀ3™¯[©ç¹áN× Èâ¸hð:eóìZÀ$ Èl­ÆÙO!Ó‚ù2çÖ@½ _1ñœïpÈ#×%³5ZôÀ@[ *[À`j5®ÅÉ­•=Vj=ÖNûÜ°ã™9 Ôƒ<C394><75>+J''uÜuÊ°= ‚€@@BE'a“
‚È hh€1ÀlÕ˜­aÓÚÐ)²ÌV¬wáB½©¾» Ï1¹j*DÈT 'ùÛ–«È´•3[§|ÈúJ\©Ít«*WMY„<59>NÛ¶]öñun;¾.ËT ˆ Â<>6€y@€/pp?¢>Œuúó kYf+ Ö»¸H­<48>ñ³W¹éöï×HÃÔÉ[ƒŸg¶NùRz@N<>LžPåä×Þ…RºxÍ <0F> ²È0XAaTûÍVÕcçØí¡4 À@Ad*z@@Âhúдó1þÕ<¬9ªººš5ö4Îy0Ìg´ÆeYJ&€ð\þR0ý&ªnÛÁªæÎ?ƒkÂt]¨«0[UsŒ<73>¾Ã|½zëdƒÇǪA@Âhú{ŒzË<C38B>ñÜ[Ûùç ¶ŸªL®â€¬SÓçIãñbE (ÌÈ»îtMæA<C3A6>ß\=prXåü#úA8ÆšÚ™\ªù@†%€  œ×Æï5Ôšà
à @x¯*3Pcz(ÇõÊÁÌÀ6Ô³<C394>*𰼡rS«qf7ÒŠ„ª.Å6Så»<C3A5>‡Õ©Nª[òöðt†ú¬“X1 õMÒ^³n-ê9&€ 2#?<foG¹<47>T•¼·1¯7në²"á1Üǽ½çÌùÏ>¹j=µYs5í·“*3õ<03>2<50>ƒaÉÖ°iÕƒ[³è!®×ëåœúO9¸Ì õ@ð,­WïšÃÁ+ß[ð¿ç×(§µFš U]<ߢŒûä<#ÔùÕ6£6y ROº¾ba¿±"ò¸®†u!<>ú¦Ø`j])Üë¨ö·Á9æˆ:þõnž¬†TÕãSà—<C3A0>¼ù¹ _ERå´m<C2B4>v¢í¢Çd¶xFî·mÛ./\ÖÍóBgm¦<6D>ò¡ZG{ý¨Õ T¨Ñš‰\µžÚ4[«ÚSse¿ SîðO€c
BeÛ‚ï.n^^r@€W ¾3¡¼¯ó|ež)¶Sfê_¡{k¹É-÷W?÷ƒÏ«qwMùEýÃê~hÏÔ&„¶ Óí|ºú¡lo<6C> SuÂQÝžœùõÜÊ9Ù“ ¸©ÖÙ*¾äyÖî<>Ô=l'{Û1÷þè<C3BE> ËmˆÙ¨Už¹“ýºµÊ^âšr´&ZM¨Q5c$ ­ŽÒŸôÀ@mì´6­³5Ϩ¸cÛQ@BÅ]ú€œÿüôI'µqcÄ&± ÓaÎ6n —M¨âeâEÕëó½x¡QÙêMuë¥>/%žW­ö/ÂÜÂÊWkpg¼^¯—'á9rhV™uäËl6õ<36>¨[ËÓ¡f<C2A1>å¤bWSz^!3Ÿ¥î%+'¿×¸ýîÿŠEÜáí y ~:§ž|¦¾0<1F>¿@k{8¼ƒpbµµpìØ!ÜÈS¹Ü@@È pUfªši: M¢5eg* ©Í´säδ6õLðdý„y¬þxÇ)Íñ%¸^¯—^$aض<C398>³…€ëÇ1ãd^éíŒ:éúqNÔÝÒÜÜþ·Æ«†
ðž(žþ¿^öØ]ê"YÅÞ~˜î„â™Rork§×¢º©œ9T\Ë™Ðxfä[à5B… 5yǶm—'©O*‡r´½b<C2BD>Óãuý÷½}½^ÙÉ{ka'Œ7ÊQ½0NsjŽi§ÚC­{ý1°‡*²’ ˜†©<Dá^ž ×ëõòòêãˆSsÙïr©ïM^#yc™+p|FÈsü”ê -Ûü,urÔ£V=ñ]ëòó·móLþâ<C3BE>_¶ü©ÜÚ<05>&aÓ*3$4ôƒpŒ1A OÙÆ2A„Q럾}úCÔëOÍïM¶íéWàÏûÿúxY~ÏØNêç… >ÇíÿÄr‰«„ó´M¢5«<D€<44>zƒ­0XE¹±ÓÚØi<C398>~´,Ëò¹óg ›Ö7 Ž!WôT­‡fؤ*™B5ö u¶æ1ƒ°,ËògpüÃôI´—5öÜT©ù<—²‚B´&ì±V­g¯é>ôo!ö¾=^eùŒpw Nqé ¨­Ùø(5hªfС{d?ªÈ ˆ BÕzÒ4X+{¬{¸5ðsCýáeYÏŽ¿ý}5û»÷c¹B@8ci„hÍf|”0†¨=c®czD†À­<C380>Šü<C5A0>Ͳ,Ëgíà³Ãž\ A"M#ûǪšœæ+D¡¢„æÇ\·†eY¾¯ù£Ö= Ad:AˆÖDzPóIÓEj'êTò#7Öƒ½·,Ëò%áá.h•™*3µj=4aHEB3í¯<C3AD>5O ð¾ùjn۶˲,Ë—ÂÿΆêuïàò8åø±ž·7Þ~* Ô3)3U{6­<>µáGœü³šeY/ß<>×½ñRY{ö2Ò$€p#A<>Ù:•‡¢ö<C2A2>ahµ™vºg¬Ë²,_<ÿñ¸ÿµ¥vé«Éä$D¾ÉaÖjìiê2 T@š M*{jEBÃ=”EXeù³Ù»0¢>4Í ¨9¦<39>:xè@[ª5•N#û© ÔeÍÊAÚ{ë²,Ë,«×OšÚi¶Šk,@¨jO2!h>foYúA@62ãç~e .˲üIäÈÕ* œÇxÂù­Uëa)¢5A ©ª¹ñ¡¬Î`™¯<E284A2>@nü<6E>c¬û²,*üô@ ¯¢aÓú.¹T/¹VÑ_ÑsŒ <0A>™Ú´´VcO¶jü˜°,Ëò'7½0¶ëíȵªæ×ãzÂu¤Ù
7ë²,Ër×OjS/õ5<07> 4éäjüÕW_å—‰½ÿO¢¦·ª)7˜Ê*­àlÖ¦}hŠõ Ò², ÎÿgEsÁ' †hÍjü?ñýøòÿˆüöÛoë½£â¶R4¯¹šö³,µ:¾+­?CZeiŽ/7qGˆ4 1^¨G×ëõòñ¡ë]#¸wàx#}” õضm—u/Xe9á»ï¾Ëœ/j¨{¸ìƒ äi¨SqyøVžÿöéõz½¼øåaYeùd¸°S+z@ˆ4T5ÿío£.˲,Ÿ#ÉE½°Ÿä"c]eY>_~Õ?þmO»Â·æ¨¨yœyYeù,ðÓ5êµ=Ò™3„Æ_üT¯Æÿú°,˲ü<C2B2>êüzµ'ƒ s­8˲,Ëç¨þÿk|Ÿ¯ñò~³'Íø Bßÿýú±,Ëò9òÁ%=Wø„óê"LEUýGÊŸòßäA×[á²,Ëržw‡ð:OaÏÞ£µ¿ÍìþtÛ¶Ëñ_ØUŸò´,ËòÈ»ëà=½¶<C2BD>|ÔZ<C394>=j¯ÿÚÂûÑúø°,Ër—íãÛøü<Ýnfk´f¤ŸÂIü6A=ãÇTÆç܆ÜüO0ù\Ì˲,Ë[ÈEž€Daj|È€ƒ0âRj®ò©ó8yõõ×_S—eYO€k> TÕŒ4<>'TÔyÍàš/òž¶2s3~‡¶,˲ܴ}ü~iŠË;JNz@Tä5ÜÓ&Õ\íõ/˲,¯Ôþw \oEŽ\áTfkEjh\W­¨*iA>\ä ¨eYå1¿ýöÛ?ü0~Ïß.¹ÕøÐÍž4 V$(Í„&ý ¢ ¸;P±mÛeYeyÉÍ¥õ¦\<5C>+:AˆÚ¬Y7{j“\Ñ3ÅSX¹´,ËòFò›© Lå¡„=uÀ^Žt&Tt uwXeyŠú/‹ÿú׿ֿâ6¼Se¦"aÔjÍhýÇMŒ=ÙNÔ±,˲<ìæ¿,æÚÛ.Èi&Dz Td¶6vRA@BŒ=ÍúUzYåéø<C3A9>š:Å :ª—ë3¹jý4A¨ì±Vc<56>øøC]eYÞÈßÿþwêyãåºõ´f“G"= â†ø=š
ÿËKõë²eYå<EFBFBD>|šŸz¹øƒp/o |§”;ÚÍïÊeYxuݶíRì}i“7ð £Ç.ûáÌT¦ü1ºý+¿eYå-x¨üǼQ?ó<>¢]Ìi0Úë¯øŒÂ²,Ëò~ä?mçU[íÅ_þò—ñî•Ÿ
Â1Æ€0Å]‰5¶ß½>/,˲¼C?üðƒßêÀë6×p*.@¨¸}P£ÝJ|”Zäv@…¿8Ƕm—eYåÛv.Ô¹žs×ÈE>ÓÌ—Wü¾ì²ß¿üü?¾aÛYѲ,ËòþñTû¶§ýߟù¸AEr¸^¯—eYåÏÇ`]eYÞÔÿP@ô“
endstream
endobj
8 0 obj
14846
endobj
9 0 obj
<< /ExtGState << /E1 << /SMask << /Type /Mask
/G 1 0 R
/S /Alpha
>>
/Type /ExtGState
>> >>
/XObject << /X2 3 0 R
/X1 7 0 R
>>
>>
endobj
10 0 obj
<< /Length 11 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
261.000000 0.000000 -0.000000 67.000000 0.000000 0.000000 cm
/X1 Do
Q
q
/E1 gs
/X2 Do
Q
endstream
endobj
11 0 obj
118
endobj
12 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 261.000000 67.000000 ]
/Resources 9 0 R
/Contents 10 0 R
/Parent 13 0 R
>>
endobj
13 0 obj
<< /Kids [ 12 0 R ]
/Count 1
/Type /Pages
>>
endobj
14 0 obj
<< /Type /Catalog
/Pages 13 0 R
>>
endobj
xref
0 15
0000000000 65535 f
0000000010 00000 n
0000000493 00000 n
0000000515 00000 n
0000009734 00000 n
0000009757 00000 n
0000031385 00000 n
0000031409 00000 n
0000046488 00000 n
0000046512 00000 n
0000046851 00000 n
0000047027 00000 n
0000047050 00000 n
0000047227 00000 n
0000047303 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 14 0 R
/Size 15
>>
startxref
47364
%%EOF

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "mastodon.large.pdf",
"filename" : "logotypeFull1.large.pdf",
"idiom" : "universal"
}
],

View File

@ -1,5 +1,7 @@
"Common.Alerts.Common.PleaseTryAgain" = "Please try again.";
"Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later.";
"Common.Alerts.DiscardPostContent.Message" = "Confirm discard composed post content.";
"Common.Alerts.DiscardPostContent.Title" = "Discard Publish";
"Common.Alerts.ServerError.Title" = "Server Error";
"Common.Alerts.SignUpFailure.Title" = "Sign Up Failure";
"Common.Alerts.VoteFailure.PollExpired" = "The poll has expired";
@ -9,6 +11,7 @@
"Common.Controls.Actions.Cancel" = "Cancel";
"Common.Controls.Actions.Confirm" = "Confirm";
"Common.Controls.Actions.Continue" = "Continue";
"Common.Controls.Actions.Discard" = "Discard";
"Common.Controls.Actions.Edit" = "Edit";
"Common.Controls.Actions.Ok" = "OK";
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
@ -35,6 +38,10 @@
"Common.Controls.Timeline.LoadMore" = "Load More";
"Common.Countable.Photo.Multiple" = "photos";
"Common.Countable.Photo.Single" = "photo";
"Scene.Compose.ComposeAction" = "Publish";
"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind";
"Scene.Compose.Title.NewPost" = "New Post";
"Scene.Compose.Title.NewReply" = "New Reply";
"Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email";
"Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App";
"Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you havent.";
@ -47,6 +54,10 @@
"Scene.ConfirmEmail.Subtitle" = "We just sent an email to %@,
tap the link to confirm your account.";
"Scene.ConfirmEmail.Title" = "One last thing.";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "See new posts";
"Scene.HomeTimeline.NavigationBarState.Offline" = "Offline";
"Scene.HomeTimeline.NavigationBarState.Published" = "Published!";
"Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post...";
"Scene.HomeTimeline.Title" = "Home";
"Scene.PublicTimeline.Title" = "Public";
"Scene.Register.Error.Item.Agreement" = "Agreement";
@ -78,8 +89,8 @@ tap the link to confirm your account.";
"Scene.Register.Input.Username.Placeholder" = "username";
"Scene.Register.Title" = "Tell us about you.";
"Scene.ServerPicker.Button.Category.All" = "All";
"Scene.ServerPicker.Button.Seeless" = "See Less";
"Scene.ServerPicker.Button.Seemore" = "See More";
"Scene.ServerPicker.Button.SeeLess" = "See Less";
"Scene.ServerPicker.Button.SeeMore" = "See More";
"Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading data. Check your internet connection.";
"Scene.ServerPicker.EmptyState.FindingServers" = "Finding available servers...";
"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own...";

View File

@ -0,0 +1,448 @@
//
// ComposeViewController.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-11.
//
import os.log
import UIKit
import Combine
import TwitterTextEditor
import Kingfisher
final class ComposeViewController: UIViewController, NeedsDependency {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>()
var viewModel: ComposeViewModel!
private var suffixedAttachmentViews: [UIView] = []
let composeTootBarButtonItem: UIBarButtonItem = {
let button = RoundedEdgesButton(type: .custom)
button.setTitle(L10n.Scene.Compose.composeAction, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold)
button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.normal.color), for: .normal)
button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.normal.color.withAlphaComponent(0.5)), for: .highlighted)
button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
button.setTitleColor(.white, for: .normal)
button.contentEdgeInsets = UIEdgeInsets(top: 3, left: 16, bottom: 3, right: 16)
button.adjustsImageWhenHighlighted = false
let barButtonItem = UIBarButtonItem(customView: button)
return barButtonItem
}()
let tableView: UITableView = {
let tableView = ControlContainableTableView()
tableView.register(ComposeRepliedToTootContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeRepliedToTootContentTableViewCell.self))
tableView.register(ComposeTootContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeTootContentTableViewCell.self))
tableView.rowHeight = UITableView.automaticDimension
tableView.separatorStyle = .none
return tableView
}()
let composeToolbarView: ComposeToolbarView = {
let composeToolbarView = ComposeToolbarView()
composeToolbarView.backgroundColor = .secondarySystemBackground
return composeToolbarView
}()
var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint!
let composeToolbarBackgroundView: UIView = {
let backgroundView = UIView()
backgroundView.backgroundColor = .secondarySystemBackground
return backgroundView
}()
}
extension ComposeViewController {
override func viewDidLoad() {
super.viewDidLoad()
viewModel.title
.receive(on: DispatchQueue.main)
.sink { [weak self] title in
guard let self = self else { return }
self.title = title
}
.store(in: &disposeBag)
view.backgroundColor = Asset.Colors.Background.systemBackground.color
navigationItem.leftBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
navigationItem.rightBarButtonItem = composeTootBarButtonItem
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
composeToolbarView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(composeToolbarView)
composeToolbarViewBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: composeToolbarView.bottomAnchor)
NSLayoutConstraint.activate([
composeToolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
composeToolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
composeToolbarViewBottomLayoutConstraint,
composeToolbarView.heightAnchor.constraint(equalToConstant: ComposeToolbarView.toolbarHeight),
])
composeToolbarView.preservesSuperviewLayoutMargins = true
composeToolbarView.delegate = self
composeToolbarBackgroundView.translatesAutoresizingMaskIntoConstraints = false
view.insertSubview(composeToolbarBackgroundView, belowSubview: composeToolbarView)
NSLayoutConstraint.activate([
composeToolbarBackgroundView.topAnchor.constraint(equalTo: composeToolbarView.topAnchor),
composeToolbarBackgroundView.leadingAnchor.constraint(equalTo: composeToolbarView.leadingAnchor),
composeToolbarBackgroundView.trailingAnchor.constraint(equalTo: composeToolbarView.trailingAnchor),
view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor),
])
tableView.delegate = self
viewModel.setupDiffableDataSource(
for: tableView,
dependency: self,
textEditorViewTextAttributesDelegate: self
)
// respond scrollView overlap change
view.layoutIfNeeded()
// update layout when keyboard show/dismiss
Publishers.CombineLatest3(
KeyboardResponderService.shared.isShow.eraseToAnyPublisher(),
KeyboardResponderService.shared.state.eraseToAnyPublisher(),
KeyboardResponderService.shared.endFrame.eraseToAnyPublisher()
)
.sink(receiveValue: { [weak self] isShow, state, endFrame in
guard let self = self else { return }
guard isShow, state == .dock else {
self.tableView.contentInset.bottom = 0.0
self.tableView.verticalScrollIndicatorInsets.bottom = 0.0
UIView.animate(withDuration: 0.3) {
self.composeToolbarViewBottomLayoutConstraint.constant = 0.0
self.view.layoutIfNeeded()
}
return
}
// isShow AND dock state
let contentFrame = self.view.convert(self.tableView.frame, to: nil)
let padding = contentFrame.maxY - endFrame.minY
guard padding > 0 else {
self.tableView.contentInset.bottom = 0.0
self.tableView.verticalScrollIndicatorInsets.bottom = 0.0
UIView.animate(withDuration: 0.3) {
self.composeToolbarViewBottomLayoutConstraint.constant = 0.0
self.view.layoutIfNeeded()
}
return
}
// add 16pt margin
self.tableView.contentInset.bottom = padding + 16
self.tableView.verticalScrollIndicatorInsets.bottom = padding + 16
UIView.animate(withDuration: 0.3) {
self.composeToolbarViewBottomLayoutConstraint.constant = padding
self.view.layoutIfNeeded()
}
})
.store(in: &disposeBag)
viewModel.isComposeTootBarButtonItemEnabled
.receive(on: DispatchQueue.main)
.assign(to: \.isEnabled, on: composeTootBarButtonItem)
.store(in: &disposeBag)
// bind custom emojis
viewModel.customEmojiViewModel
.compactMap { $0?.emojis }
.switchToLatest()
.sink(receiveValue: { [weak self] emojis in
guard let self = self else { return }
for emoji in emojis {
UITextChecker.learnWord(emoji.shortcode)
UITextChecker.learnWord(":" + emoji.shortcode + ":")
}
self.textEditorView()?.setNeedsUpdateTextAttributes()
})
.store(in: &disposeBag)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Fix AutoLayout conflict issue
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.markTextEditorViewBecomeFirstResponser()
}
}
}
extension ComposeViewController {
private func textEditorView() -> TextEditorView? {
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
let items = diffableDataSource.snapshot().itemIdentifiers
for item in items {
switch item {
case .input:
guard let indexPath = diffableDataSource.indexPath(for: item),
let cell = tableView.cellForRow(at: indexPath) as? ComposeTootContentTableViewCell else {
continue
}
return cell.textEditorView
default:
continue
}
}
return nil
}
private func markTextEditorViewBecomeFirstResponser() {
textEditorView()?.isEditing = true
}
private func showDismissConfirmAlertController() {
let alertController = UIAlertController(
title: L10n.Common.Alerts.DiscardPostContent.title,
message: L10n.Common.Alerts.DiscardPostContent.message,
preferredStyle: .alert
)
let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { [weak self] _ in
guard let self = self else { return }
self.dismiss(animated: true, completion: nil)
}
alertController.addAction(discardAction)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
}
}
extension ComposeViewController {
@objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
guard viewModel.shouldDismiss.value else {
showDismissConfirmAlertController()
return
}
dismiss(animated: true, completion: nil)
}
}
// MARK: - TextEditorViewTextAttributesDelegate
extension ComposeViewController: TextEditorViewTextAttributesDelegate {
func textEditorView(
_ textEditorView: TextEditorView,
updateAttributedString attributedString: NSAttributedString,
completion: @escaping (NSAttributedString?) -> Void
) {
DispatchQueue.global().async {
let string = attributedString.string
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update: %s", ((#file as NSString).lastPathComponent), #line, #function, string)
let stringRange = NSRange(location: 0, length: string.length)
let highlightMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)|#([^\\s.]+))")
// accept ^\B: or \s: but not accept \B: to force user input a space to make emoji take effect
// precondition :\B with following space
let emojiMatches = string.matches(pattern: "(?:(^\\B:|\\s:)([a-zA-Z0-9_]+)(:\\B(?=\\s)))")
// only accept http/https scheme
let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)")
DispatchQueue.main.async { [weak self] in
guard let self = self else {
completion(nil)
return
}
let customEmojiViewModel = self.viewModel.customEmojiViewModel.value
for view in self.suffixedAttachmentViews {
view.removeFromSuperview()
}
self.suffixedAttachmentViews.removeAll()
// set normal apperance
let attributedString = NSMutableAttributedString(attributedString: attributedString)
attributedString.removeAttribute(.suffixedAttachment, range: stringRange)
attributedString.removeAttribute(.underlineStyle, range: stringRange)
attributedString.addAttribute(.foregroundColor, value: Asset.Colors.Label.primary.color, range: stringRange)
attributedString.addAttribute(.font, value: UIFont.preferredFont(forTextStyle: .body), range: stringRange)
for match in highlightMatches {
// hashtag
if let name = string.substring(with: match, at: 2) {
let attachment: TextAttributes.SuffixedAttachment?
switch name {
// FIXME:
case "person":
attachment = .init(size: CGSize(width: 20.0, height: 20.0),
attachment: .image(UIImage(systemName: "person")!))
default:
attachment = nil
}
if let attachment = attachment {
let index = match.range.upperBound - 1
attributedString.addAttribute(
.suffixedAttachment,
value: attachment,
range: NSRange(location: index, length: 1)
)
}
}
// set highlight
var attributes = [NSAttributedString.Key: Any]()
attributes[.foregroundColor] = Asset.Colors.Label.highlight.color
// See `traitCollectionDidChange(_:)`
// set accessibility
if #available(iOS 13.0, *) {
switch self.traitCollection.accessibilityContrast {
case .high:
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
default:
break
}
}
attributedString.addAttributes(attributes, range: match.range)
}
let emojis = customEmojiViewModel?.emojis.value ?? []
if !emojis.isEmpty {
for match in emojiMatches {
guard let name = string.substring(with: match, at: 2) else { continue }
guard let emoji = emojis.first(where: { $0.shortcode == name }) else { continue }
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: handle emoji: %s", ((#file as NSString).lastPathComponent), #line, #function, name)
// set emoji token invisiable (without upper bounce space)
var attributes = [NSAttributedString.Key: Any]()
attributes[.font] = UIFont.systemFont(ofSize: 0.01)
attributedString.addAttributes(attributes, range: match.range)
// append emoji attachment
let imageViewSize = CGSize(width: 20, height: 20)
let imageView = UIImageView(frame: CGRect(origin: .zero, size: imageViewSize))
textEditorView.textContentView.addSubview(imageView)
self.suffixedAttachmentViews.append(imageView)
let processor = DownsamplingImageProcessor(size: imageViewSize)
imageView.kf.setImage(
with: URL(string: emoji.url),
placeholder: UIImage.placeholder(size: imageViewSize, color: .systemFill),
options: [
.processor(processor),
.scaleFactor(textEditorView.traitCollection.displayScale),
], completionHandler: nil
)
let layoutInTextContainer = { [weak textEditorView] (view: UIView, frame: CGRect) in
// `textEditorView` retains `textStorage`, which retains this block as a part of attributes.
guard let textEditorView = textEditorView else {
return
}
let insets = textEditorView.textContentInsets
view.frame = frame.offsetBy(dx: insets.left, dy: insets.top)
}
let attachment = TextAttributes.SuffixedAttachment(
size: imageViewSize,
attachment: .view(view: imageView, layoutInTextContainer: layoutInTextContainer)
)
let index = match.range.upperBound - 1
attributedString.addAttribute(
.suffixedAttachment,
value: attachment,
range: NSRange(location: index, length: 1)
)
}
}
for match in urlMatches {
if let name = string.substring(with: match, at: 0) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: handle emoji: %s", ((#file as NSString).lastPathComponent), #line, #function, name)
// set highlight
var attributes = [NSAttributedString.Key: Any]()
attributes[.foregroundColor] = Asset.Colors.Label.highlight.color
// See `traitCollectionDidChange(_:)`
// set accessibility
if #available(iOS 13.0, *) {
switch self.traitCollection.accessibilityContrast {
case .high:
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
default:
break
}
}
attributedString.addAttributes(attributes, range: match.range)
}
}
completion(attributedString)
}
}
}
}
// MARK: - ComposeToolbarViewDelegate
extension ComposeViewController: ComposeToolbarViewDelegate {
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, gifButtonDidPressed sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, atButtonDidPressed sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, topicButtonDidPressed sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, locationButtonDidPressed sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
// MARK: - UITableViewDelegate
extension ComposeViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
}
// MARK: - UIAdaptivePresentationControllerDelegate
extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
return viewModel.shouldDismiss.value
}
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
showDismissConfirmAlertController()
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}

View File

@ -0,0 +1,38 @@
//
// ComposeViewModel+Diffable.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-11.
//
import UIKit
import TwitterTextEditor
extension ComposeViewModel {
func setupDiffableDataSource(
for tableView: UITableView,
dependency: NeedsDependency,
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate
) {
diffableDataSource = ComposeStatusSection.tableViewDiffableDataSource(
for: tableView,
dependency: dependency,
managedObjectContext: context.managedObjectContext,
composeKind: composeKind,
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate
)
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>()
snapshot.appendSections([.repliedTo, .status])
switch composeKind {
case .reply(let statusObjectID):
snapshot.appendItems([.replyTo(statusObjectID: statusObjectID)], toSection: .repliedTo)
snapshot.appendItems([.input(replyToStatusObjectID: statusObjectID, attribute: composeStatusAttribute)], toSection: .repliedTo)
case .post:
snapshot.appendItems([.input(replyToStatusObjectID: nil, attribute: composeStatusAttribute)], toSection: .status)
}
diffableDataSource.apply(snapshot, animatingDifferences: false)
}
}

View File

@ -0,0 +1,106 @@
//
// ComposeViewModel.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-11.
//
import UIKit
import Combine
import CoreData
import CoreDataStack
final class ComposeViewModel {
var disposeBag = Set<AnyCancellable>()
// input
let context: AppContext
let composeKind: ComposeStatusSection.ComposeKind
let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute()
let composeContent = CurrentValueSubject<String, Never>("")
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
// output
var diffableDataSource: UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
// UI & UX
let title: CurrentValueSubject<String, Never>
let shouldDismiss = CurrentValueSubject<Bool, Never>(true)
let isComposeTootBarButtonItemEnabled = CurrentValueSubject<Bool, Never>(false)
// custom emojis
let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(nil)
init(
context: AppContext,
composeKind: ComposeStatusSection.ComposeKind
) {
self.context = context
self.composeKind = composeKind
switch composeKind {
case .post: self.title = CurrentValueSubject(L10n.Scene.Compose.Title.newPost)
case .reply: self.title = CurrentValueSubject(L10n.Scene.Compose.Title.newReply)
}
self.activeAuthentication = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value)
// end init
// bind active authentication
context.authenticationService.activeMastodonAuthentication
.assign(to: \.value, on: activeAuthentication)
.store(in: &disposeBag)
// bind avatar and names
activeAuthentication
.sink { [weak self] mastodonAuthentication in
guard let self = self else { return }
let mastodonUser = mastodonAuthentication?.user
let username = mastodonUser?.username ?? " "
self.composeStatusAttribute.avatarURL.value = mastodonUser?.avatarImageURL()
self.composeStatusAttribute.displayName.value = {
guard let displayName = mastodonUser?.displayName, !displayName.isEmpty else {
return username
}
return displayName
}()
self.composeStatusAttribute.username.value = username
}
.store(in: &disposeBag)
// bind compose bar button item UI state
composeStatusAttribute.composeContent
.receive(on: DispatchQueue.main)
.map { content in
let content = content?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return !content.isEmpty
}
.assign(to: \.value, on: isComposeTootBarButtonItemEnabled)
.store(in: &disposeBag)
// bind modal dismiss state
composeStatusAttribute.composeContent
.receive(on: DispatchQueue.main)
.map { content in
let content = content ?? ""
return content.isEmpty
}
.assign(to: \.value, on: shouldDismiss)
.store(in: &disposeBag)
// bind custom emojis
context.authenticationService.activeMastodonAuthenticationBox
.receive(on: DispatchQueue.main)
.sink { [weak self] activeMastodonAuthenticationBox in
guard let self = self else { return }
guard let activeMastodonAuthenticationBox = activeMastodonAuthenticationBox else { return }
let domain = activeMastodonAuthenticationBox.domain
// trigger dequeue to preload emojis
self.customEmojiViewModel.value = self.context.emojiService.dequeueCustomEmojiViewModel(for: domain)
}
.store(in: &disposeBag)
}
}

View File

@ -0,0 +1,31 @@
//
// ComposeRepliedToTootContentTableViewCell.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-11.
//
import UIKit
final class ComposeRepliedToTootContentTableViewCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension ComposeRepliedToTootContentTableViewCell {
private func _init() {
}
}

View File

@ -0,0 +1,91 @@
//
// ComposeTootContentTableViewCell.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-11.
//
import UIKit
import Combine
import TwitterTextEditor
final class ComposeTootContentTableViewCell: UITableViewCell {
var disposeBag = Set<AnyCancellable>()
let statusView = StatusView()
let textEditorView: TextEditorView = {
let textEditorView = TextEditorView()
textEditorView.font = .preferredFont(forTextStyle: .body)
textEditorView.scrollView.isScrollEnabled = false
textEditorView.isScrollEnabled = false
textEditorView.placeholderText = L10n.Scene.Compose.contentInputPlaceholder
textEditorView.keyboardType = .twitter
return textEditorView
}()
let composeContent = PassthroughSubject<String, Never>()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension ComposeTootContentTableViewCell {
private func _init() {
selectionStyle = .none
statusView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(statusView)
NSLayoutConstraint.activate([
statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
statusView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
statusView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
])
statusView.statusContainerStackView.isHidden = true
statusView.actionToolbarContainer.isHidden = true
statusView.nameTrialingDotLabel.isHidden = true
statusView.dateLabel.isHidden = true
textEditorView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(textEditorView)
NSLayoutConstraint.activate([
textEditorView.topAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10),
textEditorView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
textEditorView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: textEditorView.bottomAnchor, constant: 20),
textEditorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
])
// TODO:
textEditorView.changeObserver = self
}
override func didMoveToWindow() {
super.didMoveToWindow()
}
}
extension ComposeTootContentTableViewCell {
}
// MARK: - UITextViewDelegate
extension ComposeTootContentTableViewCell: TextEditorViewChangeObserver {
func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) {
guard changeResult.isTextChanged else { return }
composeContent.send(textEditorView.text)
}
}

View File

@ -0,0 +1,156 @@
//
// ComposeToolbarView.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-12.
//
import UIKit
protocol ComposeToolbarViewDelegate: class {
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, gifButtonDidPressed sender: UIButton)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, atButtonDidPressed sender: UIButton)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, topicButtonDidPressed sender: UIButton)
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, locationButtonDidPressed sender: UIButton)
}
final class ComposeToolbarView: UIView {
static let toolbarHeight: CGFloat = 44
weak var delegate: ComposeToolbarViewDelegate?
let mediaButton: UIButton = {
let button = UIButton(type: .custom)
button.tintColor = Asset.Colors.Button.normal.color
button.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
return button
}()
let pollButton: UIButton = {
let button = UIButton(type: .custom)
button.tintColor = Asset.Colors.Button.normal.color
button.setImage(UIImage(systemName: "list.bullet", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)), for: .normal)
return button
}()
let emojiButton: UIButton = {
let button = UIButton(type: .custom)
button.tintColor = Asset.Colors.Button.normal.color
button.setImage(UIImage(systemName: "face.smiling", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
return button
}()
let contentWarningButton: UIButton = {
let button = UIButton(type: .custom)
button.tintColor = Asset.Colors.Button.normal.color
button.setImage(UIImage(systemName: "exclamationmark.shield", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
return button
}()
let visibilityButton: UIButton = {
let button = UIButton(type: .custom)
button.tintColor = Asset.Colors.Button.normal.color
button.setImage(UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)), for: .normal)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension ComposeToolbarView {
private func _init() {
backgroundColor = .secondarySystemBackground
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 0
stackView.distribution = .fillEqually
stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
NSLayoutConstraint.activate([
stackView.centerYAnchor.constraint(equalTo: centerYAnchor),
layoutMarginsGuide.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 8), // tweak button margin offset
])
let buttons = [
mediaButton,
pollButton,
emojiButton,
contentWarningButton,
visibilityButton,
]
buttons.forEach { button in
button.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(button)
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: 44),
button.heightAnchor.constraint(equalToConstant: 44),
])
}
mediaButton.addTarget(self, action: #selector(ComposeToolbarView.cameraButtonDidPressed(_:)), for: .touchUpInside)
pollButton.addTarget(self, action: #selector(ComposeToolbarView.gifButtonDidPressed(_:)), for: .touchUpInside)
emojiButton.addTarget(self, action: #selector(ComposeToolbarView.atButtonDidPressed(_:)), for: .touchUpInside)
contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.topicButtonDidPressed(_:)), for: .touchUpInside)
visibilityButton.addTarget(self, action: #selector(ComposeToolbarView.locationButtonDidPressed(_:)), for: .touchUpInside)
}
}
extension ComposeToolbarView {
@objc private func cameraButtonDidPressed(_ sender: UIButton) {
delegate?.composeToolbarView(self, cameraButtonDidPressed: sender)
}
@objc private func gifButtonDidPressed(_ sender: UIButton) {
delegate?.composeToolbarView(self, gifButtonDidPressed: sender)
}
@objc private func atButtonDidPressed(_ sender: UIButton) {
delegate?.composeToolbarView(self, atButtonDidPressed: sender)
}
@objc private func topicButtonDidPressed(_ sender: UIButton) {
delegate?.composeToolbarView(self, topicButtonDidPressed: sender)
}
@objc private func locationButtonDidPressed(_ sender: UIButton) {
delegate?.composeToolbarView(self, locationButtonDidPressed: sender)
}
}
#if canImport(SwiftUI) && DEBUG
import SwiftUI
struct ComposeToolbarView_Previews: PreviewProvider {
static var previews: some View {
UIViewPreview(width: 375) {
let tootbarView = ComposeToolbarView()
tootbarView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tootbarView.widthAnchor.constraint(equalToConstant: 375).priority(.defaultHigh),
tootbarView.heightAnchor.constraint(equalToConstant: 64).priority(.defaultHigh),
])
return tootbarView
}
.previewLayout(.fixed(width: 375, height: 100))
}
}
#endif

View File

@ -0,0 +1,156 @@
//
// HomeTimelineNavigationBarState.swift
// Mastodon
//
// Created by sxiaojian on 2021/3/15.
//
import Combine
import Foundation
import UIKit
final class HomeTimelineNavigationBarState {
static let errorCountMax: Int = 3
var disposeBag = Set<AnyCancellable>()
var errorCountDownDispose: AnyCancellable?
var timerDispose: AnyCancellable?
var networkErrorCountSubject = PassthroughSubject<Bool, Never>()
var newTopContent = CurrentValueSubject<Bool, Never>(false)
var hasContentBeforeFetching: Bool = true
weak var viewController: HomeTimelineViewController?
let timestampUpdatePublisher = Timer.publish(every: NavigationBarProgressView.progressAnimationDuration, on: .main, in: .common)
.autoconnect()
.share()
.eraseToAnyPublisher()
init() {
reCountdown()
subscribeNewContent()
addGesture()
}
}
extension HomeTimelineNavigationBarState {
func showOfflineInNavigationBar() {
HomeTimelineNavigationBarView.progressView.removeFromSuperview()
viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.offlineView
}
func showNewPostsInNavigationBar() {
HomeTimelineNavigationBarView.progressView.removeFromSuperview()
viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.newPostsView
}
func showPublishingNewPostInNavigationBar() {
let progressView = HomeTimelineNavigationBarView.progressView
if let navigationBar = viewController?.navigationBar(), progressView.superview == nil {
navigationBar.addSubview(progressView)
NSLayoutConstraint.activate([
progressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor),
progressView.leadingAnchor.constraint(equalTo: navigationBar.leadingAnchor),
progressView.trailingAnchor.constraint(equalTo: navigationBar.trailingAnchor),
progressView.heightAnchor.constraint(equalToConstant: 3)
])
}
progressView.layoutIfNeeded()
progressView.progress = 0
viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.publishingLabel
var times: Int = 0
timerDispose = timestampUpdatePublisher
.map { _ in
times += 1
return Double(times)
}
.scan(0) { value, count in
value + 1 / pow(Double(2), count)
}
.receive(on: DispatchQueue.main)
.sink { value in
print(value)
progressView.progress = CGFloat(value)
}
}
func showPublishedInNavigationBar() {
timerDispose = nil
HomeTimelineNavigationBarView.progressView.removeFromSuperview()
viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.publishedView
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
self.showMastodonLogoInNavigationBar()
}
}
func showMastodonLogoInNavigationBar() {
HomeTimelineNavigationBarView.progressView.removeFromSuperview()
viewController?.navigationItem.titleView = HomeTimelineNavigationBarView.mastodonLogoTitleView
}
}
extension HomeTimelineNavigationBarState {
func handleScrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffsetY = scrollView.contentOffset.y
let isShowingNewPostsNew = viewController?.navigationItem.titleView === HomeTimelineNavigationBarView.newPostsView
if !isShowingNewPostsNew {
return
}
let isTop = contentOffsetY < -scrollView.contentInset.top
if isTop {
newTopContent.value = false
showMastodonLogoInNavigationBar()
}
}
func addGesture() {
let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer
tapGesture.addTarget(self, action: #selector(HomeTimelineNavigationBarState.newPostsNewDidPressed(_:)))
HomeTimelineNavigationBarView.newPostsView.addGestureRecognizer(tapGesture)
}
@objc func newPostsNewDidPressed(_ sender: UITapGestureRecognizer) {
if newTopContent.value == true {
viewController?.tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
}
}
}
extension HomeTimelineNavigationBarState {
func subscribeNewContent() {
newTopContent
.receive(on: DispatchQueue.main)
.sink { [weak self] newContent in
guard let self = self else { return }
if self.hasContentBeforeFetching, newContent {
self.showNewPostsInNavigationBar()
}
}
.store(in: &disposeBag)
}
func reCountdown() {
errorCountDownDispose = networkErrorCountSubject
.scan(0) { value, _ in value + 1 }
.sink(receiveValue: { [weak self] errorCount in
guard let self = self else { return }
if errorCount >= HomeTimelineNavigationBarState.errorCountMax {
self.showOfflineInNavigationBar()
}
})
}
func receiveCompletion(completion: Subscribers.Completion<Error>) {
switch completion {
case .failure:
networkErrorCountSubject.send(false)
case .finished:
reCountdown()
let isShowingOfflineView = viewController?.navigationItem.titleView === HomeTimelineNavigationBarView.offlineView
if isShowingOfflineView {
showMastodonLogoInNavigationBar()
}
}
}
}

View File

@ -0,0 +1,80 @@
//
// HomeTimelineNavigationBarView.swift
// Mastodon
//
// Created by sxiaojian on 2021/3/15.
//
import UIKit
final class HomeTimelineNavigationBarView {
static let mastodonLogoTitleView: UIImageView = {
let imageView = UIImageView(image: Asset.Asset.mastodonTextLogo.image.withRenderingMode(.alwaysTemplate))
imageView.tintColor = Asset.Colors.Label.primary.color
return imageView
}()
static let offlineView: UIView = {
let view = HomeTimelineNavigationBarView.backgroundViewWithColor(color: Asset.Colors.lightDangerRed.color)
let label = HomeTimelineNavigationBarView.contentLabel(text: L10n.Scene.HomeTimeline.NavigationBarState.offline)
HomeTimelineNavigationBarView.addLabelToView(label: label, view: view)
return view
}()
static let newPostsView: UIView = {
let view = HomeTimelineNavigationBarView.backgroundViewWithColor(color: Asset.Colors.Button.highlight.color)
let label = HomeTimelineNavigationBarView.contentLabel(text: L10n.Scene.HomeTimeline.NavigationBarState.newPosts)
HomeTimelineNavigationBarView.addLabelToView(label: label, view: view)
return view
}()
static var publishedView: UIView = {
let view = HomeTimelineNavigationBarView.backgroundViewWithColor(color: Asset.Colors.lightSuccessGreen.color)
let label = HomeTimelineNavigationBarView.contentLabel(text: L10n.Scene.HomeTimeline.NavigationBarState.published)
HomeTimelineNavigationBarView.addLabelToView(label: label, view: view)
return view
}()
static var progressView: NavigationBarProgressView = {
let view = NavigationBarProgressView()
return view
}()
static var publishingLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = .black
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold))
label.text = L10n.Scene.HomeTimeline.NavigationBarState.publishing
return label
}()
static func addLabelToView(label: UILabel, view: UIView) {
view.addSubview(label)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
view.trailingAnchor.constraint(equalTo: label.trailingAnchor, constant: 16),
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 1),
view.bottomAnchor.constraint(equalTo: label.bottomAnchor, constant: 1),
view.heightAnchor.constraint(equalToConstant: 24),
])
}
static func backgroundViewWithColor(color: UIColor) -> UIView {
let view = UIView()
view.backgroundColor = color
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 12
view.clipsToBounds = true
return view
}
static func contentLabel(text: String) -> UILabel {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = .white
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))
label.text = text
return label
}
}

View File

@ -80,9 +80,9 @@ extension HomeTimelineViewController {
identifier: nil,
options: [],
children: [50, 100, 150, 200, 250, 300].map { count in
UIAction(title: "Drop Recent \(count) Toots", image: nil, attributes: [], handler: { [weak self] action in
UIAction(title: "Drop Recent \(count) Statuses", image: nil, attributes: [], handler: { [weak self] action in
guard let self = self else { return }
self.dropRecentTootsAction(action, count: count)
self.dropRecentStatusAction(action, count: count)
})
}
)
@ -132,8 +132,8 @@ extension HomeTimelineViewController {
switch item {
case .homeTimelineIndex(let objectID, _):
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
let toot = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot
return toot.poll != nil
let post = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot
return post.poll != nil
default:
return false
}
@ -232,7 +232,7 @@ extension HomeTimelineViewController {
}
}
@objc private func dropRecentTootsAction(_ sender: UIAction, count: Int) {
@objc private func dropRecentStatusAction(_ sender: UIAction, count: Int) {
guard let diffableDataSource = viewModel.diffableDataSource else { return }
let snapshotTransitioning = diffableDataSource.snapshot()
@ -258,8 +258,8 @@ extension HomeTimelineViewController {
self.context.apiService.backgroundManagedObjectContext.performChanges { [weak self] in
guard let self = self else { return }
for objectID in droppingTootObjectIDs {
guard let toot = try? self.context.apiService.backgroundManagedObjectContext.existingObject(with: objectID) as? Toot else { continue }
self.context.apiService.backgroundManagedObjectContext.delete(toot)
guard let post = try? self.context.apiService.backgroundManagedObjectContext.existingObject(with: objectID) as? Toot else { continue }
self.context.apiService.backgroundManagedObjectContext.delete(post)
}
}
.sink { _ in

View File

@ -64,11 +64,7 @@ extension HomeTimelineViewController {
title = L10n.Scene.HomeTimeline.title
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
navigationItem.titleView = {
let imageView = UIImageView(image: Asset.Asset.mastodonTextLogo.image.withRenderingMode(.alwaysTemplate))
imageView.tintColor = Asset.Colors.Label.primary.color
return imageView
}()
navigationItem.titleView = HomeTimelineNavigationBarView.mastodonLogoTitleView
navigationItem.leftBarButtonItem = settingBarButtonItem
#if DEBUG
// long press to trigger debug menu
@ -101,6 +97,7 @@ extension HomeTimelineViewController {
])
viewModel.tableView = tableView
viewModel.viewController = self
viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self
tableView.delegate = self
tableView.prefetchDataSource = self
@ -168,7 +165,8 @@ extension HomeTimelineViewController {
@objc private func composeBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
let composeViewModel = ComposeViewModel(context: context, composeKind: .post)
coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
}
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
@ -209,6 +207,7 @@ extension HomeTimelineViewController {
extension HomeTimelineViewController {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
handleScrollViewDidScroll(scrollView)
self.viewModel.homeTimelineNavigationBarState.handleScrollViewDidScroll(scrollView)
}
}

View File

@ -73,6 +73,7 @@ extension HomeTimelineViewModel.LoadLatestState {
stateMachine.enter(Fail.self)
return
}
viewModel.homeTimelineNavigationBarState.hasContentBeforeFetching = !latestTootIDs.isEmpty
let end = CACurrentMediaTime()
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: collect toots id cost: %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - start)
@ -80,6 +81,7 @@ extension HomeTimelineViewModel.LoadLatestState {
viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain, authorizationBox: activeMastodonAuthenticationBox)
.receive(on: DispatchQueue.main)
.sink { completion in
viewModel.homeTimelineNavigationBarState.receiveCompletion(completion: completion)
switch completion {
case .failure(let error):
// TODO: handle error
@ -97,9 +99,12 @@ extension HomeTimelineViewModel.LoadLatestState {
let toots = response.value
let newToots = toots.filter { !latestTootIDs.contains($0.id) }
os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld new toots", ((#file as NSString).lastPathComponent), #line, #function, newToots.count)
if newToots.isEmpty {
viewModel.isFetchingLatestTimeline.value = false
viewModel.homeTimelineNavigationBarState.newTopContent.value = false
} else {
viewModel.homeTimelineNavigationBarState.newTopContent.value = true
}
}
.store(in: &viewModel.disposeBag)

View File

@ -68,6 +68,7 @@ extension HomeTimelineViewModel.LoadMiddleState {
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
.receive(on: DispatchQueue.main)
.sink { completion in
viewModel.homeTimelineNavigationBarState.receiveCompletion(completion: completion)
switch completion {
case .failure(let error):
// TODO: handle error

View File

@ -58,6 +58,7 @@ extension HomeTimelineViewModel.LoadOldestState {
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
.receive(on: DispatchQueue.main)
.sink { completion in
viewModel.homeTimelineNavigationBarState.receiveCompletion(completion: completion)
switch completion {
case .failure(let error):
os_log("%{public}s[%{public}ld], %{public}s: fetch toots failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)

View File

@ -29,9 +29,16 @@ final class HomeTimelineViewModel: NSObject {
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
let viewDidAppear = PassthroughSubject<Void, Never>()
let homeTimelineNavigationBarState = HomeTimelineNavigationBarState()
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
weak var tableView: UITableView?
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
weak var viewController: HomeTimelineViewController? {
willSet(value) {
self.homeTimelineNavigationBarState.viewController = value
}
}
// output
// top loader

View File

@ -88,8 +88,8 @@ class PickServerCell: UITableViewCell {
let expandButton: UIButton = {
let button = UIButton(type: .custom)
button.setTitle(L10n.Scene.ServerPicker.Button.seemore, for: .normal)
button.setTitle(L10n.Scene.ServerPicker.Button.seeless, for: .selected)
button.setTitle(L10n.Scene.ServerPicker.Button.seeMore, for: .normal)
button.setTitle(L10n.Scene.ServerPicker.Button.seeLess, for: .selected)
button.setTitleColor(Asset.Colors.lightBrandBlue.color, for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .footnote)
button.translatesAutoresizingMaskIntoConstraints = false

View File

@ -351,7 +351,7 @@ extension MastodonRegisterViewController {
Publishers.CombineLatest(
KeyboardResponderService.shared.state.eraseToAnyPublisher(),
KeyboardResponderService.shared.willEndFrame.eraseToAnyPublisher()
KeyboardResponderService.shared.endFrame.eraseToAnyPublisher()
)
.sink(receiveValue: { [weak self] state, endFrame in
guard let self = self else { return }

View File

@ -0,0 +1,156 @@
//
// WelcomeIllustrationView.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-1.
//
import UIKit
final class WelcomeIllustrationView: UIView {
static let artworkImageSize = CGSize(width: 375, height: 1500)
let cloudBaseImageView = UIImageView()
let rightHillImageView = UIImageView()
let leftHillImageView = UIImageView()
let centerHillImageView = UIImageView()
private let cloudBaseImage = Asset.Welcome.Illustration.cloudBase.image
private let elephantThreeOnGrassWithTreeTwoImage = Asset.Welcome.Illustration.elephantThreeOnGrassWithTreeTwo.image
private let elephantThreeOnGrassWithTreeThreeImage = Asset.Welcome.Illustration.elephantThreeOnGrassWithTreeThree.image
private let elephantThreeOnGrassImage = Asset.Welcome.Illustration.elephantThreeOnGrass.image
// layout outside
let elephantOnAirplaneWithContrailImageView: UIImageView = {
let imageView = UIImageView(image: Asset.Welcome.Illustration.elephantOnAirplaneWithContrail.image)
imageView.contentMode = .scaleAspectFill
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension WelcomeIllustrationView {
private func _init() {
backgroundColor = Asset.Welcome.Illustration.backgroundCyan.color
let topPaddingView = UIView()
topPaddingView.translatesAutoresizingMaskIntoConstraints = false
addSubview(topPaddingView)
NSLayoutConstraint.activate([
topPaddingView.topAnchor.constraint(equalTo: topAnchor),
topPaddingView.leadingAnchor.constraint(equalTo: leadingAnchor),
topPaddingView.trailingAnchor.constraint(equalTo: trailingAnchor),
])
cloudBaseImageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(cloudBaseImageView)
NSLayoutConstraint.activate([
cloudBaseImageView.topAnchor.constraint(equalTo: topPaddingView.bottomAnchor),
cloudBaseImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
cloudBaseImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
cloudBaseImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
cloudBaseImageView.widthAnchor.constraint(equalTo: cloudBaseImageView.heightAnchor, multiplier: WelcomeIllustrationView.artworkImageSize.width / WelcomeIllustrationView.artworkImageSize.height),
])
[
rightHillImageView,
leftHillImageView,
centerHillImageView,
].forEach { imageView in
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: cloudBaseImageView.topAnchor),
imageView.leadingAnchor.constraint(equalTo: cloudBaseImageView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: cloudBaseImageView.trailingAnchor),
imageView.bottomAnchor.constraint(equalTo: cloudBaseImageView.bottomAnchor),
])
}
}
override func layoutSubviews() {
super.layoutSubviews()
updateImage()
}
private func updateImage() {
let size = WelcomeIllustrationView.artworkImageSize
let width = size.width
let height = size.height
cloudBaseImageView.image = UIGraphicsImageRenderer(size: size).image { context in
// clear background
UIColor.clear.setFill()
context.fill(CGRect(origin: .zero, size: size))
// draw cloud
cloudBaseImage.draw(at: CGPoint(x: 0, y: height - cloudBaseImage.size.height))
}
rightHillImageView.image = UIGraphicsImageRenderer(size: size).image { context in
// clear background
UIColor.clear.setFill()
context.fill(CGRect(origin: .zero, size: size))
// draw elephantThreeOnGrassWithTreeTwoImage
// elephantThreeOnGrassWithTreeTwo.bottomY - 25 align to elephantThreeOnGrassImage.centerY
elephantThreeOnGrassWithTreeTwoImage.draw(at: CGPoint(x: width - elephantThreeOnGrassWithTreeTwoImage.size.width, y: height - 0.5 * elephantThreeOnGrassImage.size.height - elephantThreeOnGrassWithTreeTwoImage.size.height + 25))
}
leftHillImageView.image = UIGraphicsImageRenderer(size: size).image { context in
// clear background
UIColor.clear.setFill()
context.fill(CGRect(origin: .zero, size: size))
// draw elephantThreeOnGrassWithTreeThree
// elephantThreeOnGrassWithTreeThree.bottomY + 30 align to elephantThreeOnGrassImage.centerY
elephantThreeOnGrassWithTreeThreeImage.draw(at: CGPoint(x: 0, y: height - 0.5 * elephantThreeOnGrassImage.size.height - elephantThreeOnGrassWithTreeThreeImage.size.height - 30))
}
centerHillImageView.image = UIGraphicsImageRenderer(size: size).image { context in
// clear background
UIColor.clear.setFill()
context.fill(CGRect(origin: .zero, size: size))
// draw elephantThreeOnGrass
elephantThreeOnGrassImage.draw(at: CGPoint(x: 0, y: height - elephantThreeOnGrassImage.size.height))
}
}
}
#if canImport(SwiftUI) && DEBUG
import SwiftUI
struct WelcomeIllustrationView_Previews: PreviewProvider {
static var previews: some View {
Group {
UIViewPreview(width: 375) {
WelcomeIllustrationView()
}
.previewLayout(.fixed(width: 375, height: 1500))
UIViewPreview(width: 1125) {
WelcomeIllustrationView()
}
.previewLayout(.fixed(width: 1125, height: 5000))
}
}
}
#endif

View File

@ -13,14 +13,17 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
let welcomeIllustrationView = WelcomeIllustrationView()
var welcomeIllustrationViewBottomAnchorLayoutConstraint: NSLayoutConstraint?
private(set) lazy var logoImageView: UIImageView = {
let image = view.traitCollection.userInterfaceIdiom == .phone ? Asset.Welcome.mastodonLogo.image : Asset.Welcome.mastodonLogoLarge.image
let image = view.traitCollection.userInterfaceIdiom == .phone ? Asset.Welcome.mastodonLogo.image : Asset.Welcome.mastodonLogoBlackLarge.image
let imageView = UIImageView(image: image)
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
let sloganLabel: UILabel = {
private(set) lazy var sloganLabel: UILabel = {
let label = UILabel()
label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 34, weight: .bold))
label.textColor = Asset.Colors.Label.primary.color
@ -31,19 +34,24 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
return label
}()
let signUpButton: PrimaryActionButton = {
private(set) lazy var signUpButton: PrimaryActionButton = {
let button = PrimaryActionButton()
button.setTitle(L10n.Common.Controls.Actions.signUp, for: .normal)
let backgroundImageColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? .white : Asset.Colors.Button.highlight.color
button.setBackgroundImage(.placeholder(color: backgroundImageColor), for: .normal)
button.setBackgroundImage(.placeholder(color: backgroundImageColor.withAlphaComponent(0.9)), for: .highlighted)
let titleColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? Asset.Colors.Button.highlight.color : UIColor.white
button.setTitleColor(titleColor, for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
let signInButton: UIButton = {
private(set) lazy var signInButton: UIButton = {
let button = UIButton(type: .system)
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
button.setTitle(L10n.Common.Controls.Actions.signIn, for: .normal)
button.setTitleColor(Asset.Colors.lightBrandBlue.color, for: .normal)
button.setInsets(forContentPadding: UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0), imageTitlePadding: 0)
let titleColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? UIColor.white.withAlphaComponent(0.8) : Asset.Colors.Button.highlight.color
button.setTitleColor(titleColor, for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
@ -60,22 +68,8 @@ extension WelcomeViewController {
super.viewDidLoad()
setupOnboardingAppearance()
view.addSubview(logoImageView)
NSLayoutConstraint.activate([
logoImageView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
logoImageView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: 35),
view.readableContentGuide.trailingAnchor.constraint(equalTo: logoImageView.trailingAnchor, constant: 35),
logoImageView.heightAnchor.constraint(equalTo: logoImageView.widthAnchor, multiplier: 65.4/265.1),
])
view.addSubview(sloganLabel)
NSLayoutConstraint.activate([
sloganLabel.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: 16),
view.readableContentGuide.trailingAnchor.constraint(equalTo: sloganLabel.trailingAnchor, constant: 16),
sloganLabel.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 168),
])
setupIllustrationLayout()
view.addSubview(signInButton)
view.addSubview(signUpButton)
NSLayoutConstraint.activate([
@ -83,19 +77,126 @@ extension WelcomeViewController {
view.readableContentGuide.trailingAnchor.constraint(equalTo: signInButton.trailingAnchor, constant: WelcomeViewController.actionButtonMargin),
view.layoutMarginsGuide.bottomAnchor.constraint(equalTo: signInButton.bottomAnchor, constant: WelcomeViewController.viewBottomPaddingHeight),
signInButton.heightAnchor.constraint(equalToConstant: WelcomeViewController.actionButtonHeight).priority(.defaultHigh),
signInButton.topAnchor.constraint(equalTo: signUpButton.bottomAnchor, constant: 9),
signUpButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: WelcomeViewController.actionButtonMargin),
view.readableContentGuide.trailingAnchor.constraint(equalTo: signUpButton.trailingAnchor, constant: WelcomeViewController.actionButtonMargin),
signUpButton.heightAnchor.constraint(equalToConstant: WelcomeViewController.actionButtonHeight).priority(.defaultHigh),
])
signUpButton.addTarget(self, action: #selector(signUpButtonDidClicked(_:)), for: .touchUpInside)
signInButton.addTarget(self, action: #selector(signInButtonDidClicked(_:)), for: .touchUpInside)
}
override var preferredStatusBarStyle: UIStatusBarStyle { return .darkContent }
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
// shift illustration down for non-notch phone
var overlap: CGFloat = 5
if view.safeAreaInsets.bottom == 0 {
overlap += 56
}
welcomeIllustrationViewBottomAnchorLayoutConstraint?.constant = overlap
}
}
extension WelcomeViewController {
private func setupIllustrationLayout() {
// set logo
if logoImageView.superview == nil {
view.addSubview(logoImageView)
NSLayoutConstraint.activate([
logoImageView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
logoImageView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: 35),
view.readableContentGuide.trailingAnchor.constraint(equalTo: logoImageView.trailingAnchor, constant: 35),
logoImageView.heightAnchor.constraint(equalTo: logoImageView.widthAnchor, multiplier: 65.4/265.1),
])
logoImageView.setContentHuggingPriority(.defaultHigh, for: .vertical)
}
// set illustration for phone
if traitCollection.userInterfaceIdiom == .phone {
guard welcomeIllustrationView.superview == nil else {
return
}
welcomeIllustrationView.translatesAutoresizingMaskIntoConstraints = false
welcomeIllustrationViewBottomAnchorLayoutConstraint = welcomeIllustrationView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 5)
view.addSubview(welcomeIllustrationView)
NSLayoutConstraint.activate([
view.leftAnchor.constraint(equalTo: welcomeIllustrationView.leftAnchor, constant: 15),
welcomeIllustrationView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 15),
welcomeIllustrationViewBottomAnchorLayoutConstraint!
])
welcomeIllustrationView.cloudBaseImageView.addMotionEffect(
UIInterpolatingMotionEffect.motionEffect(minX: -5, maxX: 5, minY: -5, maxY: 5)
)
welcomeIllustrationView.rightHillImageView.addMotionEffect(
UIInterpolatingMotionEffect.motionEffect(minX: -15, maxX: 25, minY: -10, maxY: 10)
)
welcomeIllustrationView.leftHillImageView.addMotionEffect(
UIInterpolatingMotionEffect.motionEffect(minX: -25, maxX: 15, minY: -15, maxY: 15)
)
welcomeIllustrationView.centerHillImageView.addMotionEffect(
UIInterpolatingMotionEffect.motionEffect(minX: -14, maxX: 14, minY: -5, maxY: 25)
)
let topPaddingView = UIView()
topPaddingView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(topPaddingView)
NSLayoutConstraint.activate([
topPaddingView.topAnchor.constraint(equalTo: logoImageView.bottomAnchor),
topPaddingView.leadingAnchor.constraint(equalTo: logoImageView.leadingAnchor),
topPaddingView.trailingAnchor.constraint(equalTo: logoImageView.trailingAnchor),
])
welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(welcomeIllustrationView.elephantOnAirplaneWithContrailImageView)
NSLayoutConstraint.activate([
view.leftAnchor.constraint(equalTo: welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.leftAnchor, constant: 12), // add 12pt bleeding
welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.topAnchor.constraint(equalTo: topPaddingView.bottomAnchor),
// make a little bit large
welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.84),
welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.heightAnchor.constraint(equalTo: welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.widthAnchor, multiplier: 105.0/318.0),
])
let bottomPaddingView = UIView()
bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bottomPaddingView)
NSLayoutConstraint.activate([
bottomPaddingView.topAnchor.constraint(equalTo: welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.bottomAnchor),
bottomPaddingView.leadingAnchor.constraint(equalTo: logoImageView.leadingAnchor),
bottomPaddingView.trailingAnchor.constraint(equalTo: logoImageView.trailingAnchor),
bottomPaddingView.bottomAnchor.constraint(equalTo: view.centerYAnchor),
bottomPaddingView.heightAnchor.constraint(equalTo: topPaddingView.heightAnchor, multiplier: 4),
])
welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.addMotionEffect(
UIInterpolatingMotionEffect.motionEffect(minX: -20, maxX: 12, minY: -20, maxY: 12) // maxX should not larger then the bleeding (12pt)
)
view.bringSubviewToFront(logoImageView)
view.bringSubviewToFront(sloganLabel)
}
// set slogan for non-phone
if traitCollection.userInterfaceIdiom != .phone {
guard sloganLabel.superview == nil else {
return
}
view.addSubview(sloganLabel)
NSLayoutConstraint.activate([
sloganLabel.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: 16),
view.readableContentGuide.trailingAnchor.constraint(equalTo: sloganLabel.trailingAnchor, constant: 16),
sloganLabel.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 168),
])
}
view.bringSubviewToFront(sloganLabel)
view.bringSubviewToFront(logoImageView)
}
}
extension WelcomeViewController {
@ -116,6 +217,7 @@ extension WelcomeViewController: OnboardingViewControllerAppearance { }
// MARK: - UIAdaptivePresentationControllerDelegate
extension WelcomeViewController: UIAdaptivePresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
return .fullScreen
// make underneath view controller alive to fix layout issue due to view life cycle
return .overFullScreen
}
}

View File

@ -0,0 +1,19 @@
//
// RoundedEdgesButton.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-12.
//
import UIKit
final class RoundedEdgesButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
layer.masksToBounds = true
layer.cornerRadius = bounds.height * 0.5
}
}

View File

@ -0,0 +1,56 @@
//
// NavigationBarProgressView.swift
// Mastodon
//
// Created by sxiaojian on 2021/3/16.
//
import UIKit
class NavigationBarProgressView: UIView {
static let progressAnimationDuration: TimeInterval = 0.3
let sliderView: UIView = {
let view = UIView()
view.backgroundColor = Asset.Colors.buttonDefault.color
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var sliderTrailingAnchor: NSLayoutConstraint!
var progress: CGFloat = 0 {
willSet(value) {
sliderTrailingAnchor.constant = (1 - progress) * bounds.width
UIView.animate(withDuration: NavigationBarProgressView.progressAnimationDuration) {
self.setNeedsLayout()
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension NavigationBarProgressView {
func _init() {
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = .clear
addSubview(sliderView)
sliderTrailingAnchor = trailingAnchor.constraint(equalTo: sliderView.trailingAnchor)
NSLayoutConstraint.activate([
sliderView.topAnchor.constraint(equalTo: topAnchor),
sliderView.leadingAnchor.constraint(equalTo: leadingAnchor),
sliderView.bottomAnchor.constraint(equalTo: bottomAnchor),
sliderTrailingAnchor
])
}
}

View File

@ -87,6 +87,14 @@ final class StatusView: UIView {
return label
}()
let nameTrialingDotLabel: UILabel = {
let label = UILabel()
label.textColor = Asset.Colors.Label.secondary.color
label.font = .systemFont(ofSize: 17)
label.text = "·"
return label
}()
let usernameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 15, weight: .regular)
@ -284,18 +292,11 @@ extension StatusView {
nameLabel.heightAnchor.constraint(equalToConstant: 22).priority(.defaultHigh),
])
titleContainerStackView.alignment = .firstBaseline
let dotLabel: UILabel = {
let label = UILabel()
label.textColor = Asset.Colors.Label.secondary.color
label.font = .systemFont(ofSize: 17)
label.text = "·"
return label
}()
titleContainerStackView.addArrangedSubview(dotLabel)
titleContainerStackView.addArrangedSubview(nameTrialingDotLabel)
titleContainerStackView.addArrangedSubview(dateLabel)
nameLabel.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
dotLabel.setContentHuggingPriority(.defaultHigh + 2, for: .horizontal)
dotLabel.setContentCompressionResistancePriority(.required - 2, 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)
@ -376,7 +377,7 @@ extension StatusView {
NSLayoutConstraint.activate([
audioView.leadingAnchor.constraint(equalTo: statusTextContainerView.leadingAnchor),
audioView.trailingAnchor.constraint(equalTo: statusTextContainerView.trailingAnchor),
audioView.heightAnchor.constraint(equalToConstant: 44)
audioView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh)
])
// video gif
statusContainerStackView.addArrangedSubview(playerContainerView)

View File

@ -20,7 +20,6 @@ protocol StatusTableViewCellDelegate: class {
var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { get }
func statusTableViewCell(_ cell: StatusTableViewCell, playerViewControllerDidPressed playerViewController: AVPlayerViewController)
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)

View File

@ -0,0 +1,22 @@
//
// APIService+CustomEmojiViewModel.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-15.
//
import Foundation
import Combine
import CoreData
import CoreDataStack
import CommonOSLog
import DateToolsSwift
import MastodonSDK
extension APIService {
func customEmoji(domain: String) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Emoji]>, Error> {
return Mastodon.API.CustomEmojis.customEmojis(session: session, domain: domain)
}
}

View File

@ -12,7 +12,7 @@ import CoreData
import CoreDataStack
import MastodonSDK
class AuthenticationService: NSObject {
final class AuthenticationService: NSObject {
var disposeBag = Set<AnyCancellable>()
// input

View File

@ -0,0 +1,86 @@
//
// EmojiService+CustomEmojiViewModel+LoadState.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-15.
//
import os.log
import Foundation
import GameplayKit
extension EmojiService.CustomEmojiViewModel {
class LoadState: GKState {
weak var viewModel: EmojiService.CustomEmojiViewModel?
init(viewModel: EmojiService.CustomEmojiViewModel) {
self.viewModel = viewModel
}
override func didEnter(from previousState: GKState?) {
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
}
}
}
extension EmojiService.CustomEmojiViewModel.LoadState {
class Initial: EmojiService.CustomEmojiViewModel.LoadState {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return stateClass == Loading.self
}
}
class Loading: EmojiService.CustomEmojiViewModel.LoadState {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return stateClass == Fail.self || stateClass == Finish.self
}
override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState)
guard let viewModel = viewModel, let apiService = viewModel.service?.apiService, let stateMachine = stateMachine else { return }
apiService.customEmoji(domain: viewModel.domain)
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .failure(let error):
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: failed to load custom emojis for %s: %s. Retry 10s later", ((#file as NSString).lastPathComponent), #line, #function, viewModel.domain, error.localizedDescription)
stateMachine.enter(Fail.self)
case .finished:
break
}
} receiveValue: { response in
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: load %ld custom emojis for %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.count, viewModel.domain)
stateMachine.enter(Finish.self)
viewModel.emojis.value = response.value
}
.store(in: &viewModel.disposeBag)
}
}
class Fail: EmojiService.CustomEmojiViewModel.LoadState {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
return stateClass == Loading.self || stateClass == Finish.self
}
override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState)
guard let stateMachine = stateMachine else { return }
// retry 10s later
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
stateMachine.enter(Loading.self)
}
}
}
class Finish: EmojiService.CustomEmojiViewModel.LoadState {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
// one time task
return false
}
}
}

View File

@ -0,0 +1,42 @@
//
// EmojiService+CustomEmojiViewModel.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-15.
//
import Foundation
import Combine
import GameplayKit
import MastodonSDK
extension EmojiService {
final class CustomEmojiViewModel {
var disposeBag = Set<AnyCancellable>()
// input
let domain: String
weak var service: EmojiService?
// output
private(set) lazy var stateMachine: GKStateMachine = {
// exclude timeline middle fetcher state
let stateMachine = GKStateMachine(states: [
LoadState.Initial(viewModel: self),
LoadState.Loading(viewModel: self),
LoadState.Fail(viewModel: self),
LoadState.Finish(viewModel: self),
])
stateMachine.enter(LoadState.Initial.self)
return stateMachine
}()
let emojis = CurrentValueSubject<[Mastodon.Entity.Emoji], Never>([])
init(domain: String, service: EmojiService) {
self.domain = domain
self.service = service
}
}
}

View File

@ -0,0 +1,46 @@
//
// EmojiService.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-15.
//
import os.log
import Foundation
import Combine
import MastodonSDK
final class EmojiService {
weak var apiService: APIService?
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.EmojiService.working-queue")
private(set) var customEmojiViewModelDict: [String: CustomEmojiViewModel] = [:]
init(apiService: APIService) {
self.apiService = apiService
}
}
extension EmojiService {
func dequeueCustomEmojiViewModel(for domain: String) -> CustomEmojiViewModel? {
var _customEmojiViewModel: CustomEmojiViewModel?
workingQueue.sync {
if let viewModel = customEmojiViewModelDict[domain] {
_customEmojiViewModel = viewModel
} else {
let viewModel = CustomEmojiViewModel(domain: domain, service: self)
_customEmojiViewModel = viewModel
// trigger loading
viewModel.stateMachine.enter(CustomEmojiViewModel.LoadState.Loading.self)
}
}
return _customEmojiViewModel
}
}

View File

@ -18,9 +18,8 @@ final class KeyboardResponderService {
// output
let isShow = CurrentValueSubject<Bool, Never>(false)
let state = CurrentValueSubject<KeyboardState, Never>(.none)
let didEndFrame = CurrentValueSubject<CGRect, Never>(.zero)
let willEndFrame = CurrentValueSubject<CGRect, Never>(.zero)
let endFrame = CurrentValueSubject<CGRect, Never>(.zero)
private init() {
NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification, object: nil)
.sink { notification in
@ -38,15 +37,11 @@ final class KeyboardResponderService {
NotificationCenter.default.publisher(for: UIResponder.keyboardDidChangeFrameNotification, object: nil)
.sink { notification in
guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
self.didEndFrame.value = endFrame
self.updateInternalStatus(notification: notification)
}
.store(in: &disposeBag)
NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification, object: nil)
.sink { notification in
guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
self.willEndFrame.value = endFrame
self.updateInternalStatus(notification: notification)
}
.store(in: &disposeBag)
@ -62,6 +57,8 @@ extension KeyboardResponderService {
return
}
self.endFrame.value = endFrame
guard isLocal else {
self.state.value = .notLocal
return

View File

@ -23,15 +23,14 @@ class AppContext: ObservableObject {
let apiService: APIService
let authenticationService: AuthenticationService
let emojiService: EmojiService
let audioPlaybackService = AudioPlaybackService()
let videoPlaybackService = VideoPlaybackService()
let statusPrefetchingService: StatusPrefetchingService
let documentStore: DocumentStore
private var documentStoreSubscription: AnyCancellable!
let videoPlaybackService = VideoPlaybackService()
let audioPlaybackService = AudioPlaybackService()
let overrideTraitCollection = CurrentValueSubject<UITraitCollection?, Never>(nil)
init() {
@ -51,6 +50,9 @@ class AppContext: ObservableObject {
apiService: _apiService
)
emojiService = EmojiService(
apiService: apiService
)
statusPrefetchingService = StatusPrefetchingService(
apiService: _apiService
)

View File

@ -40,12 +40,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
extension AppDelegate {
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
#if DEBUG
return .all
#else
return UIDevice.current.userInterfaceIdiom == .pad ? .all : .portrait
#endif
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return UIDevice.current.userInterfaceIdiom == .phone ? .portrait : .all
}
}

View File

@ -0,0 +1,54 @@
//
// String.swift
// Example
//
// Copyright 2021 Twitter, Inc.
// SPDX-License-Identifier: Apache-2.0
//
import Foundation
extension String {
@inlinable
var length: Int {
(self as NSString).length
}
@inlinable
func substring(with range: NSRange) -> String {
(self as NSString).substring(with: range)
}
func substring(with result: NSTextCheckingResult, at index: Int) -> String? {
guard index < result.numberOfRanges else {
return nil
}
let range = result.range(at: index)
guard range.location != NSNotFound else {
return nil
}
return substring(with: result.range(at: index))
}
func firstMatch(pattern: String,
options: NSRegularExpression.Options = [],
range: NSRange? = nil) -> NSTextCheckingResult?
{
guard let regularExpression = try? NSRegularExpression(pattern: pattern, options: options) else {
return nil
}
let range = range ?? NSRange(location: 0, length: length)
return regularExpression.firstMatch(in: self, options: [], range: range)
}
func matches(pattern: String,
options: NSRegularExpression.Options = [],
range: NSRange? = nil) -> [NSTextCheckingResult]
{
guard let regularExpression = try? NSRegularExpression(pattern: pattern, options: options) else {
return []
}
let range = range ?? NSRange(location: 0, length: length)
return regularExpression.matches(in: self, options: [], range: range)
}
}

View File

@ -0,0 +1,48 @@
//
// Mastodon+API+CustomEmojis.swift
//
//
// Created by MainasuK Cirno on 2021-3-15.
//
import Foundation
import Combine
extension Mastodon.API.CustomEmojis {
static func customEmojisEndpointURL(domain: String) -> URL {
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("custom_emojis")
}
/// Custom emoji
///
/// Returns custom emojis that are available on the server.
///
/// - Since: 2.0.0
/// - Version: 3.3.0
/// # Last Update
/// 2021/3/15
/// # Reference
/// [Document](https://docs.joinmastodon.org/methods/instance/custom_emojis/)
/// - Parameters:
/// - session: `URLSession`
/// - domain: Mastodon instance domain. e.g. "example.com"
/// - Returns: `AnyPublisher` contains [`Emoji`] nested in the response
public static func customEmojis(
session: URLSession,
domain: String
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Emoji]>, Error> {
let request = Mastodon.API.get(
url: customEmojisEndpointURL(domain: domain),
query: nil,
authorization: nil
)
return session.dataTaskPublisher(for: request)
.tryMap { data, response in
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Emoji].self, from: data, response: response)
return Mastodon.Response.Content(value: value, response: response)
}
.eraseToAnyPublisher()
}
}

View File

@ -91,6 +91,8 @@ extension Mastodon.API {
extension Mastodon.API {
public enum Account { }
public enum App { }
public enum CustomEmojis { }
public enum Favorites { }
public enum Instance { }
public enum OAuth { }
public enum Onboarding { }
@ -98,7 +100,6 @@ extension Mastodon.API {
public enum Reblog { }
public enum Statuses { }
public enum Timeline { }
public enum Favorites { }
}
extension Mastodon.API {

View File

@ -53,5 +53,6 @@ arch -x86_64 pod install
- [Kingfisher](https://github.com/onevcat/Kingfisher)
- [SwiftGen](https://github.com/SwiftGen/SwiftGen)
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)
- [TwitterTextEditor](https://github.com/twitter/TwitterTextEditor)
## License