Merge pull request #74 from tootsuite/feature/compose-poll
Add poll supports for status publish
This commit is contained in:
commit
91cd7322e7
|
@ -207,6 +207,15 @@
|
||||||
"attachment_broken": "This %s is broken and can't be\nuploaded to Mastodon.",
|
"attachment_broken": "This %s is broken and can't be\nuploaded to Mastodon.",
|
||||||
"description_photo": "Describe photo for low vision people...",
|
"description_photo": "Describe photo for low vision people...",
|
||||||
"description_video": "Describe what’s happening for low vision people..."
|
"description_video": "Describe what’s happening for low vision people..."
|
||||||
|
},
|
||||||
|
"poll": {
|
||||||
|
"duration_time": "Duration: %s",
|
||||||
|
"thirty_minutes": "30 minutes",
|
||||||
|
"one_hour": "1 Hour",
|
||||||
|
"six_hours": "6 Hours",
|
||||||
|
"one_day": "1 Day",
|
||||||
|
"three_days": "3 Days",
|
||||||
|
"seven_days": "7 Days"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,6 +123,7 @@
|
||||||
DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */; };
|
DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */; };
|
||||||
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; };
|
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; };
|
||||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; };
|
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; };
|
||||||
|
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */; };
|
||||||
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; };
|
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; };
|
||||||
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; };
|
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; };
|
||||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
|
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
|
||||||
|
@ -187,6 +188,10 @@
|
||||||
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */; };
|
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */; };
|
||||||
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */; };
|
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */; };
|
||||||
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; };
|
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; };
|
||||||
|
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */; };
|
||||||
|
DB87D44B2609C11900D12C0D /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D44A2609C11900D12C0D /* PollOptionView.swift */; };
|
||||||
|
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */; };
|
||||||
|
DB87D4572609DD5300D12C0D /* DeleteBackwardResponseTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4562609DD5300D12C0D /* DeleteBackwardResponseTextField.swift */; };
|
||||||
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
|
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
|
||||||
DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; };
|
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, ); }; };
|
DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
@ -414,6 +419,7 @@
|
||||||
DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = "<group>"; };
|
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = "<group>"; };
|
||||||
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
|
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollExpiresOptionCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
|
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
|
||||||
DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -481,6 +487,10 @@
|
||||||
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToTootContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToTootContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = "<group>"; };
|
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = "<group>"; };
|
||||||
|
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
DB87D44A2609C11900D12C0D /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = "<group>"; };
|
||||||
|
DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionAppendEntryCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
DB87D4562609DD5300D12C0D /* DeleteBackwardResponseTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteBackwardResponseTextField.swift; sourceTree = "<group>"; };
|
||||||
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
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>"; };
|
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>"; };
|
DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
@ -665,6 +675,7 @@
|
||||||
2D152A8B25C295CC009AA50C /* StatusView.swift */,
|
2D152A8B25C295CC009AA50C /* StatusView.swift */,
|
||||||
2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */,
|
2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */,
|
||||||
2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */,
|
2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */,
|
||||||
|
DB87D44A2609C11900D12C0D /* PollOptionView.swift */,
|
||||||
);
|
);
|
||||||
path = Content;
|
path = Content;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -752,7 +763,6 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB45FB0425CA87B4005A8AC7 /* APIService */,
|
DB45FB0425CA87B4005A8AC7 /* APIService */,
|
||||||
DB49A61925FF327D00B98345 /* EmojiService */,
|
|
||||||
DB9A489B26036E19008B817C /* MastodonAttachmentService */,
|
DB9A489B26036E19008B817C /* MastodonAttachmentService */,
|
||||||
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
|
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
|
||||||
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */,
|
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */,
|
||||||
|
@ -843,6 +853,7 @@
|
||||||
DB9D6C1325E4F97A0051B173 /* Container */,
|
DB9D6C1325E4F97A0051B173 /* Container */,
|
||||||
DBA9B90325F1D4420012E7B6 /* Control */,
|
DBA9B90325F1D4420012E7B6 /* Control */,
|
||||||
2D152A8A25C295B8009AA50C /* Content */,
|
2D152A8A25C295B8009AA50C /* Content */,
|
||||||
|
DB87D45C2609DE6600D12C0D /* TextField */,
|
||||||
DB1D187125EF5BBD003F1F23 /* TableView */,
|
DB1D187125EF5BBD003F1F23 /* TableView */,
|
||||||
2D7631A625C1533800929FB9 /* TableviewCell */,
|
2D7631A625C1533800929FB9 /* TableviewCell */,
|
||||||
);
|
);
|
||||||
|
@ -1153,10 +1164,21 @@
|
||||||
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */,
|
DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */,
|
||||||
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */,
|
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */,
|
||||||
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */,
|
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */,
|
||||||
|
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */,
|
||||||
|
DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */,
|
||||||
|
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */,
|
||||||
);
|
);
|
||||||
path = CollectionViewCell;
|
path = CollectionViewCell;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DB87D45C2609DE6600D12C0D /* TextField */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DB87D4562609DD5300D12C0D /* DeleteBackwardResponseTextField.swift */,
|
||||||
|
);
|
||||||
|
path = TextField;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DB89B9EF25C10FD0008580ED /* CoreDataStack */ = {
|
DB89B9EF25C10FD0008580ED /* CoreDataStack */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1792,6 +1814,7 @@
|
||||||
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */,
|
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */,
|
||||||
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
||||||
2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */,
|
2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */,
|
||||||
|
DB87D4572609DD5300D12C0D /* DeleteBackwardResponseTextField.swift in Sources */,
|
||||||
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarState.swift in Sources */,
|
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarState.swift in Sources */,
|
||||||
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
||||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
||||||
|
@ -1806,6 +1829,7 @@
|
||||||
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */,
|
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */,
|
||||||
2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */,
|
2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */,
|
||||||
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
||||||
|
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,
|
||||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
||||||
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */,
|
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */,
|
||||||
DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */,
|
DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */,
|
||||||
|
@ -1818,6 +1842,7 @@
|
||||||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
||||||
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift in Sources */,
|
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift in Sources */,
|
||||||
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
|
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
|
||||||
|
DB87D44B2609C11900D12C0D /* PollOptionView.swift in Sources */,
|
||||||
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
||||||
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */,
|
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */,
|
||||||
DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */,
|
DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */,
|
||||||
|
@ -1868,6 +1893,7 @@
|
||||||
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
||||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||||
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
||||||
|
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
||||||
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
||||||
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
|
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
|
||||||
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
||||||
|
@ -1931,6 +1957,7 @@
|
||||||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
||||||
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
||||||
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */,
|
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */,
|
||||||
|
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */,
|
||||||
DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
|
DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
|
||||||
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
||||||
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
|
/// Note: update Equatable when change case
|
||||||
enum CategoryPickerItem {
|
enum CategoryPickerItem {
|
||||||
case all
|
case all
|
||||||
case category(category: Mastodon.Entity.Category)
|
case category(category: Mastodon.Entity.Category)
|
||||||
|
|
|
@ -9,12 +9,18 @@ import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import CoreData
|
import CoreData
|
||||||
|
|
||||||
|
/// Note: update Equatable when change case
|
||||||
enum ComposeStatusItem {
|
enum ComposeStatusItem {
|
||||||
case replyTo(statusObjectID: NSManagedObjectID)
|
case replyTo(statusObjectID: NSManagedObjectID)
|
||||||
case input(replyToStatusObjectID: NSManagedObjectID?, attribute: ComposeStatusAttribute)
|
case input(replyToStatusObjectID: NSManagedObjectID?, attribute: ComposeStatusAttribute)
|
||||||
case attachment(attachmentService: MastodonAttachmentService)
|
case attachment(attachmentService: MastodonAttachmentService)
|
||||||
|
case pollOption(attribute: ComposePollOptionAttribute)
|
||||||
|
case pollOptionAppendEntry
|
||||||
|
case pollExpiresOption(attribute: ComposePollExpiresOptionAttribute)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusItem: Equatable { }
|
||||||
|
|
||||||
extension ComposeStatusItem: Hashable { }
|
extension ComposeStatusItem: Hashable { }
|
||||||
|
|
||||||
extension ComposeStatusItem {
|
extension ComposeStatusItem {
|
||||||
|
@ -38,3 +44,89 @@ extension ComposeStatusItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protocol ComposePollAttributeDelegate: class {
|
||||||
|
func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusItem {
|
||||||
|
final class ComposePollOptionAttribute: Equatable, Hashable {
|
||||||
|
private let id = UUID()
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
weak var delegate: ComposePollAttributeDelegate?
|
||||||
|
|
||||||
|
let option = CurrentValueSubject<String, Never>("")
|
||||||
|
|
||||||
|
init() {
|
||||||
|
option
|
||||||
|
.sink { [weak self] option in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.delegate?.composePollAttribute(self, pollOptionDidChange: option)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
disposeBag.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: ComposePollOptionAttribute, rhs: ComposePollOptionAttribute) -> Bool {
|
||||||
|
return lhs.id == rhs.id &&
|
||||||
|
lhs.option.value == rhs.option.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusItem {
|
||||||
|
final class ComposePollExpiresOptionAttribute: Equatable, Hashable {
|
||||||
|
private let id = UUID()
|
||||||
|
|
||||||
|
let expiresOption = CurrentValueSubject<ExpiresOption, Never>(.thirtyMinutes)
|
||||||
|
|
||||||
|
|
||||||
|
static func == (lhs: ComposePollExpiresOptionAttribute, rhs: ComposePollExpiresOptionAttribute) -> Bool {
|
||||||
|
return lhs.id == rhs.id &&
|
||||||
|
lhs.expiresOption.value == rhs.expiresOption.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ExpiresOption: Equatable, Hashable, CaseIterable {
|
||||||
|
case thirtyMinutes
|
||||||
|
case oneHour
|
||||||
|
case sixHours
|
||||||
|
case oneDay
|
||||||
|
case threeDays
|
||||||
|
case sevenDays
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .thirtyMinutes: return L10n.Scene.Compose.Poll.thirtyMinutes
|
||||||
|
case .oneHour: return L10n.Scene.Compose.Poll.oneHour
|
||||||
|
case .sixHours: return L10n.Scene.Compose.Poll.sixHours
|
||||||
|
case .oneDay: return L10n.Scene.Compose.Poll.oneDay
|
||||||
|
case .threeDays: return L10n.Scene.Compose.Poll.threeDays
|
||||||
|
case .sevenDays: return L10n.Scene.Compose.Poll.sevenDays
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var seconds: Int {
|
||||||
|
switch self {
|
||||||
|
case .thirtyMinutes: return 60 * 30
|
||||||
|
case .oneHour: return 60 * 60 * 1
|
||||||
|
case .sixHours: return 60 * 60 * 6
|
||||||
|
case .oneDay: return 60 * 60 * 24
|
||||||
|
case .threeDays: return 60 * 60 * 24 * 3
|
||||||
|
case .sevenDays: return 60 * 60 * 24 * 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
|
|
||||||
|
/// Note: update Equatable when change case
|
||||||
enum PollItem {
|
enum PollItem {
|
||||||
case opion(objectID: NSManagedObjectID, attribute: Attribute)
|
case opion(objectID: NSManagedObjectID, attribute: Attribute)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ enum ComposeStatusSection: Equatable, Hashable {
|
||||||
case repliedTo
|
case repliedTo
|
||||||
case status
|
case status
|
||||||
case attachment
|
case attachment
|
||||||
|
case poll
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeStatusSection {
|
extension ComposeStatusSection {
|
||||||
|
@ -33,7 +34,10 @@ extension ComposeStatusSection {
|
||||||
managedObjectContext: NSManagedObjectContext,
|
managedObjectContext: NSManagedObjectContext,
|
||||||
composeKind: ComposeKind,
|
composeKind: ComposeKind,
|
||||||
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
||||||
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate
|
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
||||||
|
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
||||||
|
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
||||||
|
composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||||
) -> UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem> {
|
) -> UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem> {
|
||||||
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in
|
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in
|
||||||
switch item {
|
switch item {
|
||||||
|
@ -54,11 +58,11 @@ extension ComposeStatusSection {
|
||||||
}
|
}
|
||||||
ComposeStatusSection.configure(cell: cell, attribute: attribute)
|
ComposeStatusSection.configure(cell: cell, attribute: attribute)
|
||||||
cell.textEditorView.textAttributesDelegate = textEditorViewTextAttributesDelegate
|
cell.textEditorView.textAttributesDelegate = textEditorViewTextAttributesDelegate
|
||||||
// self size input cell
|
|
||||||
cell.composeContent
|
cell.composeContent
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { text in
|
.sink { text in
|
||||||
|
// self size input cell
|
||||||
collectionView.collectionViewLayout.invalidateLayout()
|
collectionView.collectionViewLayout.invalidateLayout()
|
||||||
// bind input data
|
// bind input data
|
||||||
attribute.composeContent.value = text
|
attribute.composeContent.value = text
|
||||||
|
@ -124,6 +128,30 @@ extension ComposeStatusSection {
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
return cell
|
return cell
|
||||||
|
case .pollOption(let attribute):
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionCollectionViewCell
|
||||||
|
cell.pollOptionView.optionTextField.text = attribute.option.value
|
||||||
|
cell.pollOption
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assign(to: \.value, on: attribute.option)
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
cell.delegate = composeStatusPollOptionCollectionViewCellDelegate
|
||||||
|
return cell
|
||||||
|
case .pollOptionAppendEntry:
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionAppendEntryCollectionViewCell
|
||||||
|
cell.delegate = composeStatusNewPollOptionCollectionViewCellDelegate
|
||||||
|
return cell
|
||||||
|
case .pollExpiresOption(let attribute):
|
||||||
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollExpiresOptionCollectionViewCell
|
||||||
|
cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(attribute.expiresOption.value.title), for: .normal)
|
||||||
|
attribute.expiresOption
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { expiresOption in
|
||||||
|
cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(expiresOption.title), for: .normal)
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
cell.delegate = composeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||||
|
return cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ extension PollSection {
|
||||||
pollOption option: PollOption,
|
pollOption option: PollOption,
|
||||||
pollItemAttribute attribute: PollItem.Attribute
|
pollItemAttribute attribute: PollItem.Attribute
|
||||||
) {
|
) {
|
||||||
cell.optionLabel.text = option.title
|
cell.pollOptionView.optionTextField.text = option.title
|
||||||
configure(cell: cell, selectState: attribute.selectState)
|
configure(cell: cell, selectState: attribute.selectState)
|
||||||
configure(cell: cell, voteState: attribute.voteState)
|
configure(cell: cell, voteState: attribute.voteState)
|
||||||
cell.attribute = attribute
|
cell.attribute = attribute
|
||||||
|
@ -52,35 +52,35 @@ extension PollSection {
|
||||||
static func configure(cell: PollOptionTableViewCell, selectState state: PollItem.Attribute.SelectState) {
|
static func configure(cell: PollOptionTableViewCell, selectState state: PollItem.Attribute.SelectState) {
|
||||||
switch state {
|
switch state {
|
||||||
case .none:
|
case .none:
|
||||||
cell.checkmarkBackgroundView.isHidden = true
|
cell.pollOptionView.checkmarkBackgroundView.isHidden = true
|
||||||
cell.checkmarkImageView.isHidden = true
|
cell.pollOptionView.checkmarkImageView.isHidden = true
|
||||||
case .off:
|
case .off:
|
||||||
cell.checkmarkBackgroundView.backgroundColor = .systemBackground
|
cell.pollOptionView.checkmarkBackgroundView.backgroundColor = .systemBackground
|
||||||
cell.checkmarkBackgroundView.layer.borderColor = UIColor.systemGray3.cgColor
|
cell.pollOptionView.checkmarkBackgroundView.layer.borderColor = UIColor.systemGray3.cgColor
|
||||||
cell.checkmarkBackgroundView.layer.borderWidth = 1
|
cell.pollOptionView.checkmarkBackgroundView.layer.borderWidth = 1
|
||||||
cell.checkmarkBackgroundView.isHidden = false
|
cell.pollOptionView.checkmarkBackgroundView.isHidden = false
|
||||||
cell.checkmarkImageView.isHidden = true
|
cell.pollOptionView.checkmarkImageView.isHidden = true
|
||||||
case .on:
|
case .on:
|
||||||
cell.checkmarkBackgroundView.backgroundColor = .systemBackground
|
cell.pollOptionView.checkmarkBackgroundView.backgroundColor = .systemBackground
|
||||||
cell.checkmarkBackgroundView.layer.borderColor = UIColor.clear.cgColor
|
cell.pollOptionView.checkmarkBackgroundView.layer.borderColor = UIColor.clear.cgColor
|
||||||
cell.checkmarkBackgroundView.layer.borderWidth = 0
|
cell.pollOptionView.checkmarkBackgroundView.layer.borderWidth = 0
|
||||||
cell.checkmarkBackgroundView.isHidden = false
|
cell.pollOptionView.checkmarkBackgroundView.isHidden = false
|
||||||
cell.checkmarkImageView.isHidden = false
|
cell.pollOptionView.checkmarkImageView.isHidden = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func configure(cell: PollOptionTableViewCell, voteState state: PollItem.Attribute.VoteState) {
|
static func configure(cell: PollOptionTableViewCell, voteState state: PollItem.Attribute.VoteState) {
|
||||||
switch state {
|
switch state {
|
||||||
case .hidden:
|
case .hidden:
|
||||||
cell.optionPercentageLabel.isHidden = true
|
cell.pollOptionView.optionPercentageLabel.isHidden = true
|
||||||
cell.voteProgressStripView.isHidden = true
|
cell.pollOptionView.voteProgressStripView.isHidden = true
|
||||||
cell.voteProgressStripView.setProgress(0.0, animated: false)
|
cell.pollOptionView.voteProgressStripView.setProgress(0.0, animated: false)
|
||||||
case .reveal(let voted, let percentage, let animated):
|
case .reveal(let voted, let percentage, let animated):
|
||||||
cell.optionPercentageLabel.isHidden = false
|
cell.pollOptionView.optionPercentageLabel.isHidden = false
|
||||||
cell.optionPercentageLabel.text = String(Int(100 * percentage)) + "%"
|
cell.pollOptionView.optionPercentageLabel.text = String(Int(100 * percentage)) + "%"
|
||||||
cell.voteProgressStripView.isHidden = false
|
cell.pollOptionView.voteProgressStripView.isHidden = false
|
||||||
cell.voteProgressStripView.tintColor = voted ? Asset.Colors.Background.Poll.highlight.color : Asset.Colors.Background.Poll.disabled.color
|
cell.pollOptionView.voteProgressStripView.tintColor = voted ? Asset.Colors.Background.Poll.highlight.color : Asset.Colors.Background.Poll.disabled.color
|
||||||
cell.voteProgressStripView.setProgress(CGFloat(percentage), animated: animated)
|
cell.pollOptionView.voteProgressStripView.setProgress(CGFloat(percentage), animated: animated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ internal enum Asset {
|
||||||
}
|
}
|
||||||
internal enum Circles {
|
internal enum Circles {
|
||||||
internal static let plusCircleFill = ImageAsset(name: "Circles/plus.circle.fill")
|
internal static let plusCircleFill = ImageAsset(name: "Circles/plus.circle.fill")
|
||||||
|
internal static let plusCircle = ImageAsset(name: "Circles/plus.circle")
|
||||||
}
|
}
|
||||||
internal enum Colors {
|
internal enum Colors {
|
||||||
internal enum Background {
|
internal enum Background {
|
||||||
|
@ -46,6 +47,7 @@ internal enum Asset {
|
||||||
internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background")
|
internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background")
|
||||||
internal static let systemGroupedBackground = ColorAsset(name: "Colors/Background/system.grouped.background")
|
internal static let systemGroupedBackground = ColorAsset(name: "Colors/Background/system.grouped.background")
|
||||||
internal static let tertiarySystemBackground = ColorAsset(name: "Colors/Background/tertiary.system.background")
|
internal static let tertiarySystemBackground = ColorAsset(name: "Colors/Background/tertiary.system.background")
|
||||||
|
internal static let tertiarySystemGroupedBackground = ColorAsset(name: "Colors/Background/tertiary.system.grouped.background")
|
||||||
}
|
}
|
||||||
internal enum Button {
|
internal enum Button {
|
||||||
internal static let actionToolbar = ColorAsset(name: "Colors/Button/action.toolbar")
|
internal static let actionToolbar = ColorAsset(name: "Colors/Button/action.toolbar")
|
||||||
|
|
|
@ -170,6 +170,24 @@ internal enum L10n {
|
||||||
/// Photo Library
|
/// Photo Library
|
||||||
internal static let photoLibrary = L10n.tr("Localizable", "Scene.Compose.MediaSelection.PhotoLibrary")
|
internal static let photoLibrary = L10n.tr("Localizable", "Scene.Compose.MediaSelection.PhotoLibrary")
|
||||||
}
|
}
|
||||||
|
internal enum Poll {
|
||||||
|
/// Duration: %@
|
||||||
|
internal static func durationTime(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "Scene.Compose.Poll.DurationTime", String(describing: p1))
|
||||||
|
}
|
||||||
|
/// 1 Day
|
||||||
|
internal static let oneDay = L10n.tr("Localizable", "Scene.Compose.Poll.OneDay")
|
||||||
|
/// 1 Hour
|
||||||
|
internal static let oneHour = L10n.tr("Localizable", "Scene.Compose.Poll.OneHour")
|
||||||
|
/// 7 Days
|
||||||
|
internal static let sevenDays = L10n.tr("Localizable", "Scene.Compose.Poll.SevenDays")
|
||||||
|
/// 6 Hours
|
||||||
|
internal static let sixHours = L10n.tr("Localizable", "Scene.Compose.Poll.SixHours")
|
||||||
|
/// 30 minutes
|
||||||
|
internal static let thirtyMinutes = L10n.tr("Localizable", "Scene.Compose.Poll.ThirtyMinutes")
|
||||||
|
/// 3 Days
|
||||||
|
internal static let threeDays = L10n.tr("Localizable", "Scene.Compose.Poll.ThreeDays")
|
||||||
|
}
|
||||||
internal enum Title {
|
internal enum Title {
|
||||||
/// New Post
|
/// New Post
|
||||||
internal static let newPost = L10n.tr("Localizable", "Scene.Compose.Title.NewPost")
|
internal static let newPost = L10n.tr("Localizable", "Scene.Compose.Title.NewPost")
|
||||||
|
|
15
Mastodon/Resources/Assets.xcassets/Circles/plus.circle.imageset/Contents.json
vendored
Normal file
15
Mastodon/Resources/Assets.xcassets/Circles/plus.circle.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "plus.circle.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"preserves-vector-representation" : true
|
||||||
|
}
|
||||||
|
}
|
95
Mastodon/Resources/Assets.xcassets/Circles/plus.circle.imageset/plus.circle.pdf
vendored
Normal file
95
Mastodon/Resources/Assets.xcassets/Circles/plus.circle.imageset/plus.circle.pdf
vendored
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
%PDF-1.7
|
||||||
|
|
||||||
|
1 0 obj
|
||||||
|
<< >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
2 0 obj
|
||||||
|
<< /Length 3 0 R >>
|
||||||
|
stream
|
||||||
|
/DeviceRGB CS
|
||||||
|
/DeviceRGB cs
|
||||||
|
q
|
||||||
|
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
|
||||||
|
0.000000 0.000000 0.000000 scn
|
||||||
|
21.999905 0.000160 m
|
||||||
|
34.035152 0.000160 44.000000 9.986404 44.000000 22.000080 c
|
||||||
|
44.000000 34.035332 34.013599 44.000000 21.978350 44.000000 c
|
||||||
|
9.964656 44.000000 0.000000 34.035332 0.000000 22.000080 c
|
||||||
|
0.000000 9.986404 9.986255 0.000160 21.999905 0.000160 c
|
||||||
|
h
|
||||||
|
21.999905 3.666824 m
|
||||||
|
11.819542 3.666824 3.688203 11.819717 3.688203 22.000080 c
|
||||||
|
3.688203 32.180443 11.797986 40.333336 21.978350 40.333336 c
|
||||||
|
32.158710 40.333336 40.311611 32.180443 40.333256 22.000080 c
|
||||||
|
40.354897 11.819717 32.180267 3.666824 21.999905 3.666824 c
|
||||||
|
h
|
||||||
|
13.782296 20.188307 m
|
||||||
|
20.166574 20.188307 l
|
||||||
|
20.166574 13.760918 l
|
||||||
|
20.166574 12.682493 20.899923 11.949142 21.956793 11.949142 c
|
||||||
|
23.035217 11.949142 23.790121 12.682493 23.790121 13.760918 c
|
||||||
|
23.790121 20.188307 l
|
||||||
|
30.217514 20.188307 l
|
||||||
|
31.295938 20.188307 32.029289 20.921658 32.029289 21.978525 c
|
||||||
|
32.029289 23.056950 31.295938 23.811855 30.217514 23.811855 c
|
||||||
|
23.790121 23.811855 l
|
||||||
|
23.790121 30.196133 l
|
||||||
|
23.790121 31.317715 23.035217 32.051018 21.956793 32.051018 c
|
||||||
|
20.899923 32.051018 20.166574 31.296114 20.166574 30.196133 c
|
||||||
|
20.166574 23.811855 l
|
||||||
|
13.782296 23.811855 l
|
||||||
|
12.660716 23.811855 11.927410 23.056950 11.927410 21.978525 c
|
||||||
|
11.927410 20.921658 12.682316 20.188307 13.782296 20.188307 c
|
||||||
|
h
|
||||||
|
f
|
||||||
|
n
|
||||||
|
Q
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
3 0 obj
|
||||||
|
1347
|
||||||
|
endobj
|
||||||
|
|
||||||
|
4 0 obj
|
||||||
|
<< /Annots []
|
||||||
|
/Type /Page
|
||||||
|
/MediaBox [ 0.000000 0.000000 44.000000 44.000000 ]
|
||||||
|
/Resources 1 0 R
|
||||||
|
/Contents 2 0 R
|
||||||
|
/Parent 5 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
5 0 obj
|
||||||
|
<< /Kids [ 4 0 R ]
|
||||||
|
/Count 1
|
||||||
|
/Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
6 0 obj
|
||||||
|
<< /Type /Catalog
|
||||||
|
/Pages 5 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
xref
|
||||||
|
0 7
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000010 00000 n
|
||||||
|
0000000034 00000 n
|
||||||
|
0000001437 00000 n
|
||||||
|
0000001460 00000 n
|
||||||
|
0000001633 00000 n
|
||||||
|
0000001707 00000 n
|
||||||
|
trailer
|
||||||
|
<< /ID [ (some) (id) ]
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 7
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
1766
|
||||||
|
%%EOF
|
|
@ -5,9 +5,9 @@
|
||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "255",
|
"blue" : "0xFF",
|
||||||
"green" : "255",
|
"green" : "0xFF",
|
||||||
"red" : "255"
|
"red" : "0xFF"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
@ -23,9 +23,9 @@
|
||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0.169",
|
"blue" : "0x2B",
|
||||||
"green" : "0.141",
|
"green" : "0x23",
|
||||||
"red" : "0.125"
|
"red" : "0x1F"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "232",
|
"blue" : "0xE8",
|
||||||
"green" : "225",
|
"green" : "0xE1",
|
||||||
"red" : "217"
|
"red" : "0xD9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
@ -23,9 +23,9 @@
|
||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0.169",
|
"blue" : "0x2B",
|
||||||
"green" : "0.141",
|
"green" : "0x23",
|
||||||
"red" : "0.125"
|
"red" : "0x1F"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
|
|
@ -5,9 +5,27 @@
|
||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0x43",
|
"blue" : "0xFF",
|
||||||
"green" : "0x36",
|
"green" : "0xFF",
|
||||||
"red" : "0x32"
|
"red" : "0xFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x2B",
|
||||||
|
"green" : "0x23",
|
||||||
|
"red" : "0x1F"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xE8",
|
||||||
|
"green" : "0xE1",
|
||||||
|
"red" : "0xD9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x2B",
|
||||||
|
"green" : "0x23",
|
||||||
|
"red" : "0x1F"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,6 +50,13 @@ uploaded to Mastodon.";
|
||||||
"Scene.Compose.MediaSelection.Browse" = "Browse";
|
"Scene.Compose.MediaSelection.Browse" = "Browse";
|
||||||
"Scene.Compose.MediaSelection.Camera" = "Take Photo";
|
"Scene.Compose.MediaSelection.Camera" = "Take Photo";
|
||||||
"Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library";
|
"Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library";
|
||||||
|
"Scene.Compose.Poll.DurationTime" = "Duration: %@";
|
||||||
|
"Scene.Compose.Poll.OneDay" = "1 Day";
|
||||||
|
"Scene.Compose.Poll.OneHour" = "1 Hour";
|
||||||
|
"Scene.Compose.Poll.SevenDays" = "7 Days";
|
||||||
|
"Scene.Compose.Poll.SixHours" = "6 Hours";
|
||||||
|
"Scene.Compose.Poll.ThirtyMinutes" = "30 minutes";
|
||||||
|
"Scene.Compose.Poll.ThreeDays" = "3 Days";
|
||||||
"Scene.Compose.Title.NewPost" = "New Post";
|
"Scene.Compose.Title.NewPost" = "New Post";
|
||||||
"Scene.Compose.Title.NewReply" = "New Reply";
|
"Scene.Compose.Title.NewReply" = "New Reply";
|
||||||
"Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email";
|
"Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email";
|
||||||
|
|
|
@ -66,7 +66,7 @@ extension ComposeStatusContentCollectionViewCell {
|
||||||
textEditorView.topAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10),
|
textEditorView.topAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10),
|
||||||
textEditorView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
textEditorView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||||
textEditorView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
textEditorView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
||||||
contentView.bottomAnchor.constraint(equalTo: textEditorView.bottomAnchor, constant: 20),
|
contentView.bottomAnchor.constraint(equalTo: textEditorView.bottomAnchor, constant: 10),
|
||||||
textEditorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
textEditorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
||||||
])
|
])
|
||||||
textEditorView.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
textEditorView.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
//
|
||||||
|
// ComposeStatusPollExpiresOptionCollectionViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
protocol ComposeStatusPollExpiresOptionCollectionViewCellDelegate: class {
|
||||||
|
func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusItem.ComposePollExpiresOptionAttribute.ExpiresOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ComposeStatusPollExpiresOptionCollectionViewCell: UICollectionViewCell {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
weak var delegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate?
|
||||||
|
|
||||||
|
let durationButton: UIButton = {
|
||||||
|
let button = HighlightDimmableButton()
|
||||||
|
button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12))
|
||||||
|
button.expandEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: -20, right: -20)
|
||||||
|
button.setTitle(L10n.Scene.Compose.Poll.durationTime(L10n.Scene.Compose.Poll.thirtyMinutes), for: .normal)
|
||||||
|
button.setTitleColor(Asset.Colors.Button.normal.color, for: .normal)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusPollExpiresOptionCollectionViewCell {
|
||||||
|
|
||||||
|
private typealias ExpiresOption = ComposeStatusItem.ComposePollExpiresOptionAttribute.ExpiresOption
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
durationButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(durationButton)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
durationButton.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
durationButton.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor, constant: PollOptionView.checkmarkBackgroundLeadingMargin),
|
||||||
|
durationButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
let children = ExpiresOption.allCases.map { expiresOption -> UIAction in
|
||||||
|
UIAction(title: expiresOption.title, image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] action in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.expiresOptionActionHandler(action, expiresOption: expiresOption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
durationButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
|
||||||
|
durationButton.showsMenuAsPrimaryAction = true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusPollExpiresOptionCollectionViewCell {
|
||||||
|
|
||||||
|
private func expiresOptionActionHandler(_ sender: UIAction, expiresOption: ExpiresOption) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, expiresOption.title)
|
||||||
|
delegate?.composeStatusPollExpiresOptionCollectionViewCell(self, didSelectExpiresOption: expiresOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
//
|
||||||
|
// ComposeStatusPollOptionAppendEntryCollectionViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate: class {
|
||||||
|
func composeStatusPollOptionAppendEntryCollectionViewCellDidPressed(_ cell: ComposeStatusPollOptionAppendEntryCollectionViewCell)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ComposeStatusPollOptionAppendEntryCollectionViewCell: UICollectionViewCell {
|
||||||
|
|
||||||
|
let pollOptionView = PollOptionView()
|
||||||
|
let reorderBarImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
imageView.image = UIImage(systemName: "line.horizontal.3")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)).withRenderingMode(.alwaysTemplate)
|
||||||
|
imageView.tintColor = Asset.Colors.Label.secondary.color
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let singleTagGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||||
|
|
||||||
|
weak var delegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate?
|
||||||
|
|
||||||
|
override var isHighlighted: Bool {
|
||||||
|
didSet {
|
||||||
|
pollOptionView.roundedBackgroundView.backgroundColor = isHighlighted ? Asset.Colors.Background.secondarySystemBackground.color : Asset.Colors.Background.systemBackground.color
|
||||||
|
pollOptionView.plusCircleImageView.tintColor = isHighlighted ? Asset.Colors.Button.normal.color.withAlphaComponent(0.5) : Asset.Colors.Button.normal.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||||
|
return pollOptionView.frame.contains(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
|
||||||
|
delegate = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusPollOptionAppendEntryCollectionViewCell {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
pollOptionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(pollOptionView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
pollOptionView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
pollOptionView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||||
|
pollOptionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
reorderBarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(reorderBarImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
reorderBarImageView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
reorderBarImageView.leadingAnchor.constraint(equalTo: pollOptionView.trailingAnchor, constant: ComposeStatusPollOptionCollectionViewCell.reorderHandlerImageLeadingMargin),
|
||||||
|
reorderBarImageView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||||
|
reorderBarImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
pollOptionView.checkmarkImageView.isHidden = true
|
||||||
|
pollOptionView.checkmarkBackgroundView.isHidden = true
|
||||||
|
pollOptionView.optionPercentageLabel.isHidden = true
|
||||||
|
pollOptionView.optionTextField.isHidden = true
|
||||||
|
pollOptionView.plusCircleImageView.isHidden = false
|
||||||
|
|
||||||
|
pollOptionView.roundedBackgroundView.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||||
|
setupBorderColor()
|
||||||
|
|
||||||
|
pollOptionView.addGestureRecognizer(singleTagGestureRecognizer)
|
||||||
|
singleTagGestureRecognizer.addTarget(self, action: #selector(ComposeStatusPollOptionAppendEntryCollectionViewCell.singleTagGestureRecognizerHandler(_:)))
|
||||||
|
|
||||||
|
reorderBarImageView.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupBorderColor() {
|
||||||
|
pollOptionView.roundedBackgroundView.layer.borderWidth = 1
|
||||||
|
pollOptionView.roundedBackgroundView.layer.borderColor = Asset.Colors.Background.secondarySystemBackground.color.cgColor
|
||||||
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
|
||||||
|
setupBorderColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusPollOptionAppendEntryCollectionViewCell {
|
||||||
|
|
||||||
|
@objc private func singleTagGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
delegate?.composeStatusPollOptionAppendEntryCollectionViewCellDidPressed(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(SwiftUI) && DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ComposeStatusNewPollOptionCollectionViewCell_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var controls: some View {
|
||||||
|
Group {
|
||||||
|
UIViewPreview() {
|
||||||
|
let cell = ComposeStatusPollOptionAppendEntryCollectionViewCell()
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 44 + 10))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
controls.colorScheme(.light)
|
||||||
|
controls.colorScheme(.dark)
|
||||||
|
}
|
||||||
|
.background(Color.gray)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,168 @@
|
||||||
|
//
|
||||||
|
// ComposeStatusPollOptionCollectionViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
protocol ComposeStatusPollOptionCollectionViewCellDelegate: class {
|
||||||
|
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?)
|
||||||
|
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ComposeStatusPollOptionCollectionViewCell: UICollectionViewCell {
|
||||||
|
|
||||||
|
static let reorderHandlerImageLeadingMargin: CGFloat = 11
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
weak var delegate: ComposeStatusPollOptionCollectionViewCellDelegate?
|
||||||
|
|
||||||
|
let pollOptionView = PollOptionView()
|
||||||
|
let reorderBarImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
imageView.image = UIImage(systemName: "line.horizontal.3")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)).withRenderingMode(.alwaysTemplate)
|
||||||
|
imageView.tintColor = Asset.Colors.Label.secondary.color
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let singleTagGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||||
|
|
||||||
|
private var pollOptionSubscription: AnyCancellable?
|
||||||
|
let pollOption = PassthroughSubject<String, Never>()
|
||||||
|
|
||||||
|
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||||
|
return pollOptionView.frame.contains(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
|
||||||
|
delegate = nil
|
||||||
|
disposeBag.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusPollOptionCollectionViewCell {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
pollOptionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(pollOptionView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
pollOptionView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
pollOptionView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||||
|
pollOptionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
reorderBarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(reorderBarImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
reorderBarImageView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
reorderBarImageView.leadingAnchor.constraint(equalTo: pollOptionView.trailingAnchor, constant: ComposeStatusPollOptionCollectionViewCell.reorderHandlerImageLeadingMargin),
|
||||||
|
reorderBarImageView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||||
|
reorderBarImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
pollOptionView.checkmarkImageView.isHidden = true
|
||||||
|
pollOptionView.optionPercentageLabel.isHidden = true
|
||||||
|
pollOptionView.optionTextField.text = nil
|
||||||
|
|
||||||
|
pollOptionView.roundedBackgroundView.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||||
|
pollOptionView.checkmarkBackgroundView.backgroundColor = Asset.Colors.Background.tertiarySystemBackground.color
|
||||||
|
setupBorderColor()
|
||||||
|
|
||||||
|
pollOptionView.addGestureRecognizer(singleTagGestureRecognizer)
|
||||||
|
singleTagGestureRecognizer.addTarget(self, action: #selector(ComposeStatusPollOptionCollectionViewCell.singleTagGestureRecognizerHandler(_:)))
|
||||||
|
|
||||||
|
pollOptionSubscription = NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: pollOptionView.optionTextField)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] notification in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let textField = notification.object as? UITextField else { return }
|
||||||
|
self.pollOption.send(textField.text ?? "")
|
||||||
|
}
|
||||||
|
pollOptionView.optionTextField.deleteBackwardDelegate = self
|
||||||
|
pollOptionView.optionTextField.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupBorderColor() {
|
||||||
|
pollOptionView.checkmarkBackgroundView.layer.borderColor = UIColor.systemGray3.cgColor
|
||||||
|
pollOptionView.checkmarkBackgroundView.layer.borderWidth = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
|
||||||
|
setupBorderColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposeStatusPollOptionCollectionViewCell {
|
||||||
|
|
||||||
|
@objc private func singleTagGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
pollOptionView.optionTextField.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - DeleteBackwardResponseTextFieldDelegate
|
||||||
|
extension ComposeStatusPollOptionCollectionViewCell: DeleteBackwardResponseTextFieldDelegate {
|
||||||
|
func deleteBackwardResponseTextField(_ textField: DeleteBackwardResponseTextField, textBeforeDelete: String?) {
|
||||||
|
delegate?.composeStatusPollOptionCollectionViewCell(self, textBeforeDeleteBackward: textBeforeDelete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITextFieldDelegate
|
||||||
|
extension ComposeStatusPollOptionCollectionViewCell: UITextFieldDelegate {
|
||||||
|
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
if textField === pollOptionView.optionTextField {
|
||||||
|
delegate?.composeStatusPollOptionCollectionViewCell(self, pollOptionTextFieldDidReturn: textField)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(SwiftUI) && DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ComposeStatusPollOptionCollectionViewCell_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var controls: some View {
|
||||||
|
Group {
|
||||||
|
UIViewPreview() {
|
||||||
|
let cell = ComposeStatusPollOptionCollectionViewCell()
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 44 + 10))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
controls.colorScheme(.light)
|
||||||
|
controls.colorScheme(.dark)
|
||||||
|
}
|
||||||
|
.background(Color.gray)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -45,6 +45,9 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
collectionView.register(ComposeRepliedToTootContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeRepliedToTootContentCollectionViewCell.self))
|
collectionView.register(ComposeRepliedToTootContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeRepliedToTootContentCollectionViewCell.self))
|
||||||
collectionView.register(ComposeStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self))
|
collectionView.register(ComposeStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self))
|
||||||
collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self))
|
collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self))
|
||||||
|
collectionView.register(ComposeStatusPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self))
|
||||||
|
collectionView.register(ComposeStatusPollOptionAppendEntryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self))
|
||||||
|
collectionView.register(ComposeStatusPollExpiresOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self))
|
||||||
collectionView.backgroundColor = Asset.Colors.Background.systemBackground.color
|
collectionView.backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||||
return collectionView
|
return collectionView
|
||||||
}()
|
}()
|
||||||
|
@ -147,15 +150,17 @@ extension ComposeViewController {
|
||||||
])
|
])
|
||||||
|
|
||||||
collectionView.delegate = self
|
collectionView.delegate = self
|
||||||
// Note: do not allow reorder due to the images display order following the upload time
|
|
||||||
// let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ComposeViewController.longPressReorderGestureHandler(_:)))
|
|
||||||
// collectionView.addGestureRecognizer(longPressReorderGesture)
|
|
||||||
viewModel.setupDiffableDataSource(
|
viewModel.setupDiffableDataSource(
|
||||||
for: collectionView,
|
for: collectionView,
|
||||||
dependency: self,
|
dependency: self,
|
||||||
textEditorViewTextAttributesDelegate: self,
|
textEditorViewTextAttributesDelegate: self,
|
||||||
composeStatusAttachmentTableViewCellDelegate: self
|
composeStatusAttachmentTableViewCellDelegate: self,
|
||||||
|
composeStatusPollOptionCollectionViewCellDelegate: self,
|
||||||
|
composeStatusNewPollOptionCollectionViewCellDelegate: self,
|
||||||
|
composeStatusPollExpiresOptionCollectionViewCellDelegate: self
|
||||||
)
|
)
|
||||||
|
let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ComposeViewController.longPressReorderGestureHandler(_:)))
|
||||||
|
collectionView.addGestureRecognizer(longPressReorderGesture)
|
||||||
|
|
||||||
// respond scrollView overlap change
|
// respond scrollView overlap change
|
||||||
view.layoutIfNeeded()
|
view.layoutIfNeeded()
|
||||||
|
@ -206,6 +211,16 @@ extension ComposeViewController {
|
||||||
.assign(to: \.isEnabled, on: publishBarButtonItem)
|
.assign(to: \.isEnabled, on: publishBarButtonItem)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
viewModel.isMediaToolbarButtonEnabled
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assign(to: \.isEnabled, on: composeToolbarView.mediaButton)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
viewModel.isPollToolbarButtonEnabled
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assign(to: \.isEnabled, on: composeToolbarView.pollButton)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// bind custom emojis
|
// bind custom emojis
|
||||||
viewModel.customEmojiViewModel
|
viewModel.customEmojiViewModel
|
||||||
.compactMap { $0?.emojis }
|
.compactMap { $0?.emojis }
|
||||||
|
@ -268,6 +283,57 @@ extension ComposeViewController {
|
||||||
textEditorView()?.isEditing = true
|
textEditorView()?.isEditing = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func pollOptionCollectionViewCell(of item: ComposeStatusItem) -> ComposeStatusPollOptionCollectionViewCell? {
|
||||||
|
guard case .pollOption = item else { return nil }
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
||||||
|
guard let indexPath = diffableDataSource.indexPath(for: item),
|
||||||
|
let cell = collectionView.cellForItem(at: indexPath) as? ComposeStatusPollOptionCollectionViewCell else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
private func firstPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? {
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
||||||
|
let items = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll)
|
||||||
|
let firstPollItem = items.first { item -> Bool in
|
||||||
|
guard case .pollOption = item else { return false }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let item = firstPollItem else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return pollOptionCollectionViewCell(of: item)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func lastPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? {
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
||||||
|
let items = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll)
|
||||||
|
let lastPollItem = items.last { item -> Bool in
|
||||||
|
guard case .pollOption = item else { return false }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let item = lastPollItem else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return pollOptionCollectionViewCell(of: item)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func markFirstPollOptionCollectionViewCellBecomeFirstResponser() {
|
||||||
|
guard let cell = firstPollOptionCollectionViewCell() else { return }
|
||||||
|
cell.pollOptionView.optionTextField.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func markLastPollOptionCollectionViewCellBecomeFirstResponser() {
|
||||||
|
guard let cell = lastPollOptionCollectionViewCell() else { return }
|
||||||
|
cell.pollOptionView.optionTextField.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
private func showDismissConfirmAlertController() {
|
private func showDismissConfirmAlertController() {
|
||||||
let alertController = UIAlertController(
|
let alertController = UIAlertController(
|
||||||
title: L10n.Common.Alerts.DiscardPostContent.title,
|
title: L10n.Common.Alerts.DiscardPostContent.title,
|
||||||
|
@ -322,13 +388,20 @@ extension ComposeViewController {
|
||||||
dismiss(animated: true, completion: nil)
|
dismiss(animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Do not allow reorder image due to image display order following the update time
|
// seealso: ComposeViewModel.setupDiffableDataSource(…)
|
||||||
@objc private func longPressReorderGestureHandler(_ sender: UILongPressGestureRecognizer) {
|
@objc private func longPressReorderGestureHandler(_ sender: UILongPressGestureRecognizer) {
|
||||||
switch(sender.state) {
|
switch(sender.state) {
|
||||||
case .began:
|
case .began:
|
||||||
guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)) else {
|
guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)),
|
||||||
|
let cell = collectionView.cellForItem(at: selectedIndexPath) as? ComposeStatusPollOptionCollectionViewCell else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
// check if pressing reorder bar no not
|
||||||
|
let locationInCell = sender.location(in: cell)
|
||||||
|
guard cell.reorderBarImageView.frame.contains(locationInCell) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
|
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
|
||||||
case .changed:
|
case .changed:
|
||||||
guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)),
|
guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)),
|
||||||
|
@ -336,19 +409,20 @@ extension ComposeViewController {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
guard let item = diffableDataSource.itemIdentifier(for: selectedIndexPath),
|
guard let item = diffableDataSource.itemIdentifier(for: selectedIndexPath),
|
||||||
case .attachment = item else {
|
case .pollOption = item else {
|
||||||
collectionView.cancelInteractiveMovement()
|
collectionView.cancelInteractiveMovement()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
collectionView.updateInteractiveMovementTargetPosition(sender.location(in: collectionView))
|
var position = sender.location(in: collectionView)
|
||||||
|
position.x = collectionView.frame.width * 0.5
|
||||||
|
collectionView.updateInteractiveMovementTargetPosition(position)
|
||||||
case .ended:
|
case .ended:
|
||||||
collectionView.endInteractiveMovement()
|
collectionView.endInteractiveMovement()
|
||||||
default:
|
default:
|
||||||
collectionView.cancelInteractiveMovement()
|
collectionView.cancelInteractiveMovement()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -490,7 +564,6 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
||||||
extension ComposeViewController: ComposeToolbarViewDelegate {
|
extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||||
|
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType: ComposeToolbarView.MediaSelectionType) {
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType: ComposeToolbarView.MediaSelectionType) {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: %s", ((#file as NSString).lastPathComponent), #line, #function, mediaSelectionType.rawValue)
|
|
||||||
switch mediaSelectionType {
|
switch mediaSelectionType {
|
||||||
case .photoLibrary:
|
case .photoLibrary:
|
||||||
present(imagePicker, animated: true, completion: nil)
|
present(imagePicker, animated: true, completion: nil)
|
||||||
|
@ -501,20 +574,31 @@ extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, gifButtonDidPressed sender: UIButton) {
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, pollButtonDidPressed sender: UIButton) {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
viewModel.isPollComposing.value.toggle()
|
||||||
|
|
||||||
|
// setup initial poll option if needs
|
||||||
|
if viewModel.isPollComposing.value, viewModel.pollOptionAttributes.value.isEmpty {
|
||||||
|
viewModel.pollOptionAttributes.value = [ComposeStatusItem.ComposePollOptionAttribute(), ComposeStatusItem.ComposePollOptionAttribute()]
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.isPollComposing.value {
|
||||||
|
// Magic RunLoop
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.markFirstPollOptionCollectionViewCellBecomeFirstResponser()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
markTextEditorViewBecomeFirstResponser()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, atButtonDidPressed sender: UIButton) {
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed 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) {
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed 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) {
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton) {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -554,7 +638,6 @@ extension ComposeViewController: PHPickerViewControllerDelegate {
|
||||||
pickerResult: result,
|
pickerResult: result,
|
||||||
initalAuthenticationBox: viewModel.activeAuthenticationBox.value
|
initalAuthenticationBox: viewModel.activeAuthenticationBox.value
|
||||||
)
|
)
|
||||||
service.delegate = viewModel
|
|
||||||
return service
|
return service
|
||||||
}
|
}
|
||||||
viewModel.attachmentServices.value = viewModel.attachmentServices.value + attachmentServices
|
viewModel.attachmentServices.value = viewModel.attachmentServices.value + attachmentServices
|
||||||
|
@ -574,7 +657,6 @@ extension ComposeViewController: UIImagePickerControllerDelegate & UINavigationC
|
||||||
image: image,
|
image: image,
|
||||||
initalAuthenticationBox: viewModel.activeAuthenticationBox.value
|
initalAuthenticationBox: viewModel.activeAuthenticationBox.value
|
||||||
)
|
)
|
||||||
attachmentService.delegate = viewModel
|
|
||||||
viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService]
|
viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -598,7 +680,6 @@ extension ComposeViewController: UIDocumentPickerDelegate {
|
||||||
imageData: imageData,
|
imageData: imageData,
|
||||||
initalAuthenticationBox: viewModel.activeAuthenticationBox.value
|
initalAuthenticationBox: viewModel.activeAuthenticationBox.value
|
||||||
)
|
)
|
||||||
attachmentService.delegate = viewModel
|
|
||||||
viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService]
|
viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService]
|
||||||
} catch {
|
} catch {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
os_log("%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
|
@ -622,3 +703,95 @@ extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelega
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - ComposeStatusPollOptionCollectionViewCellDelegate
|
||||||
|
extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelegate {
|
||||||
|
|
||||||
|
// handle delete backward event for poll option input
|
||||||
|
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?) {
|
||||||
|
guard (text ?? "").isEmpty else { return }
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
guard let indexPath = collectionView.indexPath(for: cell) else { return }
|
||||||
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
guard case let .pollOption(attribute) = item else { return }
|
||||||
|
|
||||||
|
var pollAttributes = viewModel.pollOptionAttributes.value
|
||||||
|
guard let index = pollAttributes.firstIndex(of: attribute) else { return }
|
||||||
|
|
||||||
|
// mark previous (fallback to next) item of removed middle poll option become first responder
|
||||||
|
let pollItems = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll)
|
||||||
|
if let indexOfItem = pollItems.firstIndex(of: item), index > 0 {
|
||||||
|
func cellBeforeRemoved() -> ComposeStatusPollOptionCollectionViewCell? {
|
||||||
|
guard index > 0 else { return nil }
|
||||||
|
let indexBeforeRemoved = pollItems.index(before: indexOfItem)
|
||||||
|
let itemBeforeRemoved = pollItems[indexBeforeRemoved]
|
||||||
|
return pollOptionCollectionViewCell(of: itemBeforeRemoved)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cellAfterRemoved() -> ComposeStatusPollOptionCollectionViewCell? {
|
||||||
|
guard index < pollItems.count - 1 else { return nil }
|
||||||
|
let indexAfterRemoved = pollItems.index(after: index)
|
||||||
|
let itemAfterRemoved = pollItems[indexAfterRemoved]
|
||||||
|
return pollOptionCollectionViewCell(of: itemAfterRemoved)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cell: ComposeStatusPollOptionCollectionViewCell? = cellBeforeRemoved()
|
||||||
|
if cell == nil {
|
||||||
|
cell = cellAfterRemoved()
|
||||||
|
}
|
||||||
|
cell?.pollOptionView.optionTextField.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard pollAttributes.count > 2 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pollAttributes.remove(at: index)
|
||||||
|
|
||||||
|
// update data source
|
||||||
|
viewModel.pollOptionAttributes.value = pollAttributes
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle keyboard return event for poll option input
|
||||||
|
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField) {
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
guard let indexPath = collectionView.indexPath(for: cell) else { return }
|
||||||
|
let pollItems = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll).filter { item in
|
||||||
|
guard case .pollOption = item else { return false }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
guard let index = pollItems.firstIndex(of: item) else { return }
|
||||||
|
|
||||||
|
if index == pollItems.count - 1 {
|
||||||
|
// is the last
|
||||||
|
viewModel.createNewPollOptionIfPossible()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.markLastPollOptionCollectionViewCellBecomeFirstResponser()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// not the last
|
||||||
|
let indexAfter = pollItems.index(after: index)
|
||||||
|
let itemAfter = pollItems[indexAfter]
|
||||||
|
let cell = pollOptionCollectionViewCell(of: itemAfter)
|
||||||
|
cell?.pollOptionView.optionTextField.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate
|
||||||
|
extension ComposeViewController: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate {
|
||||||
|
func composeStatusPollOptionAppendEntryCollectionViewCellDidPressed(_ cell: ComposeStatusPollOptionAppendEntryCollectionViewCell) {
|
||||||
|
viewModel.createNewPollOptionIfPossible()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.markLastPollOptionCollectionViewCellBecomeFirstResponser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ComposeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||||
|
extension ComposeViewController: ComposeStatusPollExpiresOptionCollectionViewCellDelegate {
|
||||||
|
func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusItem.ComposePollExpiresOptionAttribute.ExpiresOption) {
|
||||||
|
viewModel.pollExpiresOptionAttribute.expiresOption.value = expiresOption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,10 @@ extension ComposeViewModel {
|
||||||
for collectionView: UICollectionView,
|
for collectionView: UICollectionView,
|
||||||
dependency: NeedsDependency,
|
dependency: NeedsDependency,
|
||||||
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
||||||
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate
|
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
||||||
|
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
|
||||||
|
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
|
||||||
|
composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||||
) {
|
) {
|
||||||
let diffableDataSource = ComposeStatusSection.collectionViewDiffableDataSource(
|
let diffableDataSource = ComposeStatusSection.collectionViewDiffableDataSource(
|
||||||
for: collectionView,
|
for: collectionView,
|
||||||
|
@ -22,33 +25,36 @@ extension ComposeViewModel {
|
||||||
managedObjectContext: context.managedObjectContext,
|
managedObjectContext: context.managedObjectContext,
|
||||||
composeKind: composeKind,
|
composeKind: composeKind,
|
||||||
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate,
|
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate,
|
||||||
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate
|
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate,
|
||||||
|
composeStatusPollOptionCollectionViewCellDelegate: composeStatusPollOptionCollectionViewCellDelegate,
|
||||||
|
composeStatusNewPollOptionCollectionViewCellDelegate: composeStatusNewPollOptionCollectionViewCellDelegate,
|
||||||
|
composeStatusPollExpiresOptionCollectionViewCellDelegate: composeStatusPollExpiresOptionCollectionViewCellDelegate
|
||||||
)
|
)
|
||||||
|
|
||||||
// Note: do not allow reorder due to the images display order following the upload time
|
diffableDataSource.reorderingHandlers.canReorderItem = { item in
|
||||||
// diffableDataSource.reorderingHandlers.canReorderItem = { item in
|
switch item {
|
||||||
// switch item {
|
case .pollOption: return true
|
||||||
// case .attachment: return true
|
default: return false
|
||||||
// default: return false
|
}
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// }
|
// update reordered data source
|
||||||
// diffableDataSource.reorderingHandlers.didReorder = { [weak self] transaction in
|
diffableDataSource.reorderingHandlers.didReorder = { [weak self] transaction in
|
||||||
// guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
//
|
|
||||||
// let items = transaction.finalSnapshot.itemIdentifiers
|
let items = transaction.finalSnapshot.itemIdentifiers
|
||||||
// var attachmentServices: [MastodonAttachmentService] = []
|
var pollOptionAttributes: [ComposeStatusItem.ComposePollOptionAttribute] = []
|
||||||
// for item in items {
|
for item in items {
|
||||||
// guard case let .attachment(attachmentService) = item else { continue }
|
guard case let .pollOption(attribute) = item else { continue }
|
||||||
// attachmentServices.append(attachmentService)
|
pollOptionAttributes.append(attribute)
|
||||||
// }
|
}
|
||||||
// self.attachmentServices.value = attachmentServices
|
self.pollOptionAttributes.value = pollOptionAttributes
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
|
|
||||||
self.diffableDataSource = diffableDataSource
|
self.diffableDataSource = diffableDataSource
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>()
|
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>()
|
||||||
snapshot.appendSections([.repliedTo, .status, .attachment])
|
snapshot.appendSections([.repliedTo, .status, .attachment, .poll])
|
||||||
switch composeKind {
|
switch composeKind {
|
||||||
case .reply(let statusObjectID):
|
case .reply(let statusObjectID):
|
||||||
snapshot.appendItems([.replyTo(statusObjectID: statusObjectID)], toSection: .repliedTo)
|
snapshot.appendItems([.replyTo(statusObjectID: statusObjectID)], toSection: .repliedTo)
|
||||||
|
|
|
@ -53,6 +53,15 @@ extension ComposeViewModel.PublishState {
|
||||||
let mediaIDs = attachmentServices.compactMap { attachmentService in
|
let mediaIDs = attachmentServices.compactMap { attachmentService in
|
||||||
attachmentService.attachment.value?.id
|
attachmentService.attachment.value?.id
|
||||||
}
|
}
|
||||||
|
let pollOptions: [String]? = {
|
||||||
|
guard viewModel.isPollComposing.value else { return nil }
|
||||||
|
return viewModel.pollOptionAttributes.value.map { attribute in attribute.option.value }
|
||||||
|
}()
|
||||||
|
let pollExpiresIn: Int? = {
|
||||||
|
guard viewModel.isPollComposing.value else { return nil }
|
||||||
|
return viewModel.pollExpiresOptionAttribute.expiresOption.value.seconds
|
||||||
|
}()
|
||||||
|
|
||||||
let updateMediaQuerySubscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = {
|
let updateMediaQuerySubscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = {
|
||||||
var subscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = []
|
var subscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = []
|
||||||
for attachmentService in attachmentServices {
|
for attachmentService in attachmentServices {
|
||||||
|
@ -81,7 +90,9 @@ extension ComposeViewModel.PublishState {
|
||||||
.flatMap { attachments -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> in
|
.flatMap { attachments -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> in
|
||||||
let query = Mastodon.API.Statuses.PublishStatusQuery(
|
let query = Mastodon.API.Statuses.PublishStatusQuery(
|
||||||
status: viewModel.composeStatusAttribute.composeContent.value,
|
status: viewModel.composeStatusAttribute.composeContent.value,
|
||||||
mediaIDs: mediaIDs
|
mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs,
|
||||||
|
pollOptions: pollOptions,
|
||||||
|
pollExpiresIn: pollExpiresIn
|
||||||
)
|
)
|
||||||
return viewModel.context.apiService.publishStatus(
|
return viewModel.context.apiService.publishStatus(
|
||||||
domain: domain,
|
domain: domain,
|
||||||
|
|
|
@ -19,6 +19,7 @@ final class ComposeViewModel {
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let composeKind: ComposeStatusSection.ComposeKind
|
let composeKind: ComposeStatusSection.ComposeKind
|
||||||
let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute()
|
let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute()
|
||||||
|
let isPollComposing = CurrentValueSubject<Bool, Never>(false)
|
||||||
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
|
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
|
||||||
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
||||||
|
|
||||||
|
@ -41,6 +42,8 @@ final class ComposeViewModel {
|
||||||
let title: CurrentValueSubject<String, Never>
|
let title: CurrentValueSubject<String, Never>
|
||||||
let shouldDismiss = CurrentValueSubject<Bool, Never>(true)
|
let shouldDismiss = CurrentValueSubject<Bool, Never>(true)
|
||||||
let isPublishBarButtonItemEnabled = CurrentValueSubject<Bool, Never>(false)
|
let isPublishBarButtonItemEnabled = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
let isMediaToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
|
||||||
|
let isPollToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
|
||||||
|
|
||||||
// custom emojis
|
// custom emojis
|
||||||
let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(nil)
|
let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(nil)
|
||||||
|
@ -48,6 +51,10 @@ final class ComposeViewModel {
|
||||||
// attachment
|
// attachment
|
||||||
let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([])
|
let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([])
|
||||||
|
|
||||||
|
// polls
|
||||||
|
let pollOptionAttributes = CurrentValueSubject<[ComposeStatusItem.ComposePollOptionAttribute], Never>([])
|
||||||
|
let pollExpiresOptionAttribute = ComposeStatusItem.ComposePollExpiresOptionAttribute()
|
||||||
|
|
||||||
init(
|
init(
|
||||||
context: AppContext,
|
context: AppContext,
|
||||||
composeKind: ComposeStatusSection.ComposeKind
|
composeKind: ComposeStatusSection.ComposeKind
|
||||||
|
@ -98,19 +105,48 @@ final class ComposeViewModel {
|
||||||
.map { services in
|
.map { services in
|
||||||
services.allSatisfy { $0.uploadStateMachineSubject.value is MastodonAttachmentService.UploadState.Finish }
|
services.allSatisfy { $0.uploadStateMachineSubject.value is MastodonAttachmentService.UploadState.Finish }
|
||||||
}
|
}
|
||||||
Publishers.CombineLatest4(
|
let isPollAttributeAllValid = pollOptionAttributes
|
||||||
|
.map { pollAttributes in
|
||||||
|
pollAttributes.allSatisfy { attribute -> Bool in
|
||||||
|
!attribute.option.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4(
|
||||||
isComposeContentEmpty.eraseToAnyPublisher(),
|
isComposeContentEmpty.eraseToAnyPublisher(),
|
||||||
isComposeContentValid.eraseToAnyPublisher(),
|
isComposeContentValid.eraseToAnyPublisher(),
|
||||||
isMediaEmpty.eraseToAnyPublisher(),
|
isMediaEmpty.eraseToAnyPublisher(),
|
||||||
isMediaUploadAllSuccess.eraseToAnyPublisher()
|
isMediaUploadAllSuccess.eraseToAnyPublisher()
|
||||||
)
|
)
|
||||||
.map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess in
|
.map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in
|
||||||
if isMediaEmpty {
|
if isMediaEmpty {
|
||||||
return isComposeContentValid && !isComposeContentEmpty
|
return isComposeContentValid && !isComposeContentEmpty
|
||||||
} else {
|
} else {
|
||||||
return isComposeContentValid && isMediaUploadAllSuccess
|
return isComposeContentValid && isMediaUploadAllSuccess
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
|
let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest4(
|
||||||
|
isComposeContentEmpty.eraseToAnyPublisher(),
|
||||||
|
isComposeContentValid.eraseToAnyPublisher(),
|
||||||
|
isPollComposing.eraseToAnyPublisher(),
|
||||||
|
isPollAttributeAllValid.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollAttributeAllValid -> Bool in
|
||||||
|
if isPollComposing {
|
||||||
|
return isComposeContentValid && !isComposeContentEmpty && isPollAttributeAllValid
|
||||||
|
} else {
|
||||||
|
return isComposeContentValid && !isComposeContentEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
isPublishBarButtonItemEnabledPrecondition1,
|
||||||
|
isPublishBarButtonItemEnabledPrecondition2
|
||||||
|
)
|
||||||
|
.map { $0 && $1 }
|
||||||
.assign(to: \.value, on: isPublishBarButtonItemEnabled)
|
.assign(to: \.value, on: isPublishBarButtonItemEnabled)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
@ -137,49 +173,108 @@ final class ComposeViewModel {
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// bind snapshot and drive service upload state
|
// bind snapshot
|
||||||
attachmentServices
|
Publishers.CombineLatest3(
|
||||||
.receive(on: DispatchQueue.main)
|
attachmentServices.eraseToAnyPublisher(),
|
||||||
.sink { [weak self] attachmentServices in
|
isPollComposing.eraseToAnyPublisher(),
|
||||||
guard let self = self else { return }
|
pollOptionAttributes.eraseToAnyPublisher()
|
||||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
)
|
||||||
var snapshot = diffableDataSource.snapshot()
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] attachmentServices, isPollComposing, pollAttributes in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||||
|
var snapshot = diffableDataSource.snapshot()
|
||||||
|
|
||||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .attachment))
|
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .attachment))
|
||||||
var items: [ComposeStatusItem] = []
|
var attachmentItems: [ComposeStatusItem] = []
|
||||||
for attachmentService in attachmentServices {
|
for attachmentService in attachmentServices {
|
||||||
let item = ComposeStatusItem.attachment(attachmentService: attachmentService)
|
let item = ComposeStatusItem.attachment(attachmentService: attachmentService)
|
||||||
items.append(item)
|
attachmentItems.append(item)
|
||||||
|
}
|
||||||
|
snapshot.appendItems(attachmentItems, toSection: .attachment)
|
||||||
|
|
||||||
|
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .poll))
|
||||||
|
if isPollComposing {
|
||||||
|
var pollItems: [ComposeStatusItem] = []
|
||||||
|
for pollAttribute in pollAttributes {
|
||||||
|
let item = ComposeStatusItem.pollOption(attribute: pollAttribute)
|
||||||
|
pollItems.append(item)
|
||||||
}
|
}
|
||||||
snapshot.appendItems(items, toSection: .attachment)
|
snapshot.appendItems(pollItems, toSection: .poll)
|
||||||
|
if pollAttributes.count < 4 {
|
||||||
|
snapshot.appendItems([ComposeStatusItem.pollOptionAppendEntry], toSection: .poll)
|
||||||
|
}
|
||||||
|
snapshot.appendItems([ComposeStatusItem.pollExpiresOption(attribute: self.pollExpiresOptionAttribute)], toSection: .poll)
|
||||||
|
}
|
||||||
|
|
||||||
diffableDataSource.apply(snapshot)
|
diffableDataSource.apply(snapshot)
|
||||||
|
|
||||||
// make image upload in the queue
|
// drive service upload state
|
||||||
for attachmentService in attachmentServices {
|
// make image upload in the queue
|
||||||
// skip when prefix N task when task finish OR fail OR uploading
|
for attachmentService in attachmentServices {
|
||||||
guard let currentState = attachmentService.uploadStateMachine.currentState else { break }
|
// skip when prefix N task when task finish OR fail OR uploading
|
||||||
if currentState is MastodonAttachmentService.UploadState.Fail {
|
guard let currentState = attachmentService.uploadStateMachine.currentState else { break }
|
||||||
continue
|
if currentState is MastodonAttachmentService.UploadState.Fail {
|
||||||
}
|
continue
|
||||||
if currentState is MastodonAttachmentService.UploadState.Finish {
|
}
|
||||||
continue
|
if currentState is MastodonAttachmentService.UploadState.Finish {
|
||||||
}
|
continue
|
||||||
if currentState is MastodonAttachmentService.UploadState.Uploading {
|
}
|
||||||
break
|
if currentState is MastodonAttachmentService.UploadState.Uploading {
|
||||||
}
|
break
|
||||||
// trigger uploading one by one
|
}
|
||||||
if currentState is MastodonAttachmentService.UploadState.Initial {
|
// trigger uploading one by one
|
||||||
attachmentService.uploadStateMachine.enter(MastodonAttachmentService.UploadState.Uploading.self)
|
if currentState is MastodonAttachmentService.UploadState.Initial {
|
||||||
break
|
attachmentService.uploadStateMachine.enter(MastodonAttachmentService.UploadState.Uploading.self)
|
||||||
}
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind delegate
|
||||||
|
attachmentServices
|
||||||
|
.sink { [weak self] attachmentServices in
|
||||||
|
guard let self = self else { return }
|
||||||
|
attachmentServices.forEach { $0.delegate = self }
|
||||||
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
pollOptionAttributes
|
||||||
|
.sink { [weak self] pollAttributes in
|
||||||
|
guard let self = self else { return }
|
||||||
|
pollAttributes.forEach { $0.delegate = self }
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind compose toolbar UI state
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
isPollComposing.eraseToAnyPublisher(),
|
||||||
|
attachmentServices.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink(receiveValue: { [weak self] isPollComposing, attachmentServices in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let shouldMediaDisable = isPollComposing || attachmentServices.count >= 4
|
||||||
|
let shouldPollDisable = attachmentServices.count > 0
|
||||||
|
|
||||||
|
self.isMediaToolbarButtonEnabled.value = !shouldMediaDisable
|
||||||
|
self.isPollToolbarButtonEnabled.value = !shouldPollDisable
|
||||||
|
})
|
||||||
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ComposeViewModel {
|
||||||
|
func createNewPollOptionIfPossible() {
|
||||||
|
guard pollOptionAttributes.value.count < 4 else { return }
|
||||||
|
|
||||||
|
let attribute = ComposeStatusItem.ComposePollOptionAttribute()
|
||||||
|
pollOptionAttributes.value = pollOptionAttributes.value + [attribute]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - MastodonAttachmentServiceDelegate
|
// MARK: - MastodonAttachmentServiceDelegate
|
||||||
extension ComposeViewModel: MastodonAttachmentServiceDelegate {
|
extension ComposeViewModel: MastodonAttachmentServiceDelegate {
|
||||||
func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) {
|
func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) {
|
||||||
|
@ -187,3 +282,11 @@ extension ComposeViewModel: MastodonAttachmentServiceDelegate {
|
||||||
attachmentServices.value = attachmentServices.value
|
attachmentServices.value = attachmentServices.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - ComposePollAttributeDelegate
|
||||||
|
extension ComposeViewModel: ComposePollAttributeDelegate {
|
||||||
|
func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?) {
|
||||||
|
// trigger update
|
||||||
|
pollOptionAttributes.value = pollOptionAttributes.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,14 +5,15 @@
|
||||||
// Created by MainasuK Cirno on 2021-3-12.
|
// Created by MainasuK Cirno on 2021-3-12.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol ComposeToolbarViewDelegate: class {
|
protocol ComposeToolbarViewDelegate: class {
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType: ComposeToolbarView.MediaSelectionType)
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType: ComposeToolbarView.MediaSelectionType)
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, gifButtonDidPressed sender: UIButton)
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, pollButtonDidPressed sender: UIButton)
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, atButtonDidPressed sender: UIButton)
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: UIButton)
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, topicButtonDidPressed sender: UIButton)
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton)
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, locationButtonDidPressed sender: UIButton)
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ComposeToolbarView: UIView {
|
final class ComposeToolbarView: UIView {
|
||||||
|
@ -102,10 +103,10 @@ extension ComposeToolbarView {
|
||||||
|
|
||||||
mediaButton.menu = createMediaContextMenu()
|
mediaButton.menu = createMediaContextMenu()
|
||||||
mediaButton.showsMenuAsPrimaryAction = true
|
mediaButton.showsMenuAsPrimaryAction = true
|
||||||
pollButton.addTarget(self, action: #selector(ComposeToolbarView.gifButtonDidPressed(_:)), for: .touchUpInside)
|
pollButton.addTarget(self, action: #selector(ComposeToolbarView.pollButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
emojiButton.addTarget(self, action: #selector(ComposeToolbarView.atButtonDidPressed(_:)), for: .touchUpInside)
|
emojiButton.addTarget(self, action: #selector(ComposeToolbarView.emojiButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.topicButtonDidPressed(_:)), for: .touchUpInside)
|
contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.contentWarningButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
visibilityButton.addTarget(self, action: #selector(ComposeToolbarView.locationButtonDidPressed(_:)), for: .touchUpInside)
|
visibilityButton.addTarget(self, action: #selector(ComposeToolbarView.visibilityButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,18 +132,21 @@ extension ComposeToolbarView {
|
||||||
var children: [UIMenuElement] = []
|
var children: [UIMenuElement] = []
|
||||||
let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
|
let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .photoLibaray", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
self.delegate?.composeToolbarView(self, cameraButtonDidPressed: self.mediaButton, mediaSelectionType: .photoLibrary)
|
self.delegate?.composeToolbarView(self, cameraButtonDidPressed: self.mediaButton, mediaSelectionType: .photoLibrary)
|
||||||
}
|
}
|
||||||
children.append(photoLibraryAction)
|
children.append(photoLibraryAction)
|
||||||
if UIImagePickerController.isSourceTypeAvailable(.camera) {
|
if UIImagePickerController.isSourceTypeAvailable(.camera) {
|
||||||
let cameraAction = UIAction(title: L10n.Scene.Compose.MediaSelection.camera, image: UIImage(systemName: "camera"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in
|
let cameraAction = UIAction(title: L10n.Scene.Compose.MediaSelection.camera, image: UIImage(systemName: "camera"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .camera", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
self.delegate?.composeToolbarView(self, cameraButtonDidPressed: self.mediaButton, mediaSelectionType: .camera)
|
self.delegate?.composeToolbarView(self, cameraButtonDidPressed: self.mediaButton, mediaSelectionType: .camera)
|
||||||
})
|
})
|
||||||
children.append(cameraAction)
|
children.append(cameraAction)
|
||||||
}
|
}
|
||||||
let browseAction = UIAction(title: L10n.Scene.Compose.MediaSelection.browse, image: UIImage(systemName: "ellipsis"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
|
let browseAction = UIAction(title: L10n.Scene.Compose.MediaSelection.browse, image: UIImage(systemName: "ellipsis"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .browse", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
self.delegate?.composeToolbarView(self, cameraButtonDidPressed: self.mediaButton, mediaSelectionType: .browse)
|
self.delegate?.composeToolbarView(self, cameraButtonDidPressed: self.mediaButton, mediaSelectionType: .browse)
|
||||||
}
|
}
|
||||||
children.append(browseAction)
|
children.append(browseAction)
|
||||||
|
@ -155,20 +159,24 @@ extension ComposeToolbarView {
|
||||||
|
|
||||||
extension ComposeToolbarView {
|
extension ComposeToolbarView {
|
||||||
|
|
||||||
@objc private func gifButtonDidPressed(_ sender: UIButton) {
|
@objc private func pollButtonDidPressed(_ sender: UIButton) {
|
||||||
delegate?.composeToolbarView(self, gifButtonDidPressed: sender)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
delegate?.composeToolbarView(self, pollButtonDidPressed: sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func atButtonDidPressed(_ sender: UIButton) {
|
@objc private func emojiButtonDidPressed(_ sender: UIButton) {
|
||||||
delegate?.composeToolbarView(self, atButtonDidPressed: sender)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
delegate?.composeToolbarView(self, emojiButtonDidPressed: sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func topicButtonDidPressed(_ sender: UIButton) {
|
@objc private func contentWarningButtonDidPressed(_ sender: UIButton) {
|
||||||
delegate?.composeToolbarView(self, topicButtonDidPressed: sender)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
delegate?.composeToolbarView(self, contentWarningButtonDidPressed: sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func locationButtonDidPressed(_ sender: UIButton) {
|
@objc private func visibilityButtonDidPressed(_ sender: UIButton) {
|
||||||
delegate?.composeToolbarView(self, locationButtonDidPressed: sender)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
delegate?.composeToolbarView(self, visibilityButtonDidPressed: sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,201 @@
|
||||||
|
//
|
||||||
|
// PollOptionView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
final class PollOptionView: UIView {
|
||||||
|
|
||||||
|
static let height: CGFloat = optionHeight + 2 * verticalMargin
|
||||||
|
static let optionHeight: CGFloat = 44
|
||||||
|
static let verticalMargin: CGFloat = 5
|
||||||
|
static let checkmarkImageSize = CGSize(width: 26, height: 26)
|
||||||
|
static let checkmarkBackgroundLeadingMargin: CGFloat = 9
|
||||||
|
|
||||||
|
private var viewStateDisposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
let roundedBackgroundView = UIView()
|
||||||
|
let voteProgressStripView: StripProgressView = {
|
||||||
|
let view = StripProgressView()
|
||||||
|
view.tintColor = Asset.Colors.Background.Poll.highlight.color
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
let checkmarkBackgroundView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .systemBackground
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
let checkmarkImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 14, weight: .bold))!
|
||||||
|
imageView.image = image.withRenderingMode(.alwaysTemplate)
|
||||||
|
imageView.tintColor = Asset.Colors.Button.normal.color
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let plusCircleImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
let image = Asset.Circles.plusCircle.image
|
||||||
|
imageView.image = image.withRenderingMode(.alwaysTemplate)
|
||||||
|
imageView.tintColor = Asset.Colors.Button.normal.color
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let optionTextField: DeleteBackwardResponseTextField = {
|
||||||
|
let textField = DeleteBackwardResponseTextField()
|
||||||
|
textField.font = .systemFont(ofSize: 15, weight: .medium)
|
||||||
|
textField.textColor = Asset.Colors.Label.primary.color
|
||||||
|
textField.text = "Option"
|
||||||
|
textField.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .left : .right
|
||||||
|
return textField
|
||||||
|
}()
|
||||||
|
|
||||||
|
let optionLabelMiddlePaddingView = UIView()
|
||||||
|
|
||||||
|
let optionPercentageLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 13, weight: .regular)
|
||||||
|
label.textColor = Asset.Colors.Label.primary.color
|
||||||
|
label.text = "50%"
|
||||||
|
label.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .right : .left
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PollOptionView {
|
||||||
|
private func _init() {
|
||||||
|
roundedBackgroundView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||||
|
|
||||||
|
roundedBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(roundedBackgroundView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
roundedBackgroundView.topAnchor.constraint(equalTo: topAnchor, constant: 5),
|
||||||
|
roundedBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
roundedBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
bottomAnchor.constraint(equalTo: roundedBackgroundView.bottomAnchor, constant: 5),
|
||||||
|
roundedBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionView.optionHeight).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
voteProgressStripView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
roundedBackgroundView.addSubview(voteProgressStripView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
voteProgressStripView.topAnchor.constraint(equalTo: roundedBackgroundView.topAnchor),
|
||||||
|
voteProgressStripView.leadingAnchor.constraint(equalTo: roundedBackgroundView.leadingAnchor),
|
||||||
|
voteProgressStripView.trailingAnchor.constraint(equalTo: roundedBackgroundView.trailingAnchor),
|
||||||
|
voteProgressStripView.bottomAnchor.constraint(equalTo: roundedBackgroundView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
checkmarkBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
roundedBackgroundView.addSubview(checkmarkBackgroundView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
checkmarkBackgroundView.topAnchor.constraint(equalTo: roundedBackgroundView.topAnchor, constant: 9),
|
||||||
|
checkmarkBackgroundView.leadingAnchor.constraint(equalTo: roundedBackgroundView.leadingAnchor, constant: PollOptionView.checkmarkBackgroundLeadingMargin),
|
||||||
|
roundedBackgroundView.bottomAnchor.constraint(equalTo: checkmarkBackgroundView.bottomAnchor, constant: 9),
|
||||||
|
checkmarkBackgroundView.widthAnchor.constraint(equalToConstant: PollOptionView.checkmarkImageSize.width).priority(.required - 1),
|
||||||
|
checkmarkBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionView.checkmarkImageSize.height).priority(.required - 1),
|
||||||
|
])
|
||||||
|
|
||||||
|
checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
checkmarkBackgroundView.addSubview(checkmarkImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
checkmarkImageView.topAnchor.constraint(equalTo: checkmarkBackgroundView.topAnchor, constant: 5),
|
||||||
|
checkmarkImageView.leadingAnchor.constraint(equalTo: checkmarkBackgroundView.leadingAnchor, constant: 5),
|
||||||
|
checkmarkBackgroundView.trailingAnchor.constraint(equalTo: checkmarkImageView.trailingAnchor, constant: 5),
|
||||||
|
checkmarkBackgroundView.bottomAnchor.constraint(equalTo: checkmarkImageView.bottomAnchor, constant: 5),
|
||||||
|
])
|
||||||
|
|
||||||
|
plusCircleImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(plusCircleImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
plusCircleImageView.topAnchor.constraint(equalTo: checkmarkBackgroundView.topAnchor),
|
||||||
|
plusCircleImageView.leadingAnchor.constraint(equalTo: checkmarkBackgroundView.leadingAnchor),
|
||||||
|
plusCircleImageView.trailingAnchor.constraint(equalTo: checkmarkBackgroundView.trailingAnchor),
|
||||||
|
plusCircleImageView.bottomAnchor.constraint(equalTo: checkmarkBackgroundView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
optionTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
roundedBackgroundView.addSubview(optionTextField)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
optionTextField.leadingAnchor.constraint(equalTo: checkmarkBackgroundView.trailingAnchor, constant: 14),
|
||||||
|
optionTextField.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
||||||
|
optionTextField.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
||||||
|
])
|
||||||
|
|
||||||
|
optionLabelMiddlePaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
roundedBackgroundView.addSubview(optionLabelMiddlePaddingView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
optionLabelMiddlePaddingView.leadingAnchor.constraint(equalTo: optionTextField.trailingAnchor),
|
||||||
|
optionLabelMiddlePaddingView.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
||||||
|
optionLabelMiddlePaddingView.heightAnchor.constraint(equalToConstant: 4).priority(.defaultHigh),
|
||||||
|
optionLabelMiddlePaddingView.widthAnchor.constraint(greaterThanOrEqualToConstant: 8).priority(.defaultLow),
|
||||||
|
])
|
||||||
|
optionLabelMiddlePaddingView.setContentHuggingPriority(.required - 1, for: .horizontal)
|
||||||
|
optionLabelMiddlePaddingView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
|
|
||||||
|
optionPercentageLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
roundedBackgroundView.addSubview(optionPercentageLabel)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
optionPercentageLabel.leadingAnchor.constraint(equalTo: optionLabelMiddlePaddingView.trailingAnchor),
|
||||||
|
roundedBackgroundView.trailingAnchor.constraint(equalTo: optionPercentageLabel.trailingAnchor, constant: 18),
|
||||||
|
optionPercentageLabel.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
||||||
|
])
|
||||||
|
optionPercentageLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||||
|
optionPercentageLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||||
|
|
||||||
|
plusCircleImageView.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
updateCornerRadius()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PollOptionView {
|
||||||
|
private func updateCornerRadius() {
|
||||||
|
roundedBackgroundView.layer.masksToBounds = true
|
||||||
|
roundedBackgroundView.layer.cornerRadius = PollOptionView.optionHeight * 0.5
|
||||||
|
roundedBackgroundView.layer.cornerCurve = .circular
|
||||||
|
|
||||||
|
checkmarkBackgroundView.layer.masksToBounds = true
|
||||||
|
checkmarkBackgroundView.layer.cornerRadius = PollOptionView.checkmarkImageSize.width * 0.5
|
||||||
|
checkmarkBackgroundView.layer.cornerCurve = .circular
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(SwiftUI) && DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PollOptionView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
PollOptionView()
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 100))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -10,55 +10,9 @@ import Combine
|
||||||
|
|
||||||
final class PollOptionTableViewCell: UITableViewCell {
|
final class PollOptionTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
static let height: CGFloat = optionHeight + 2 * verticalMargin
|
let pollOptionView = PollOptionView()
|
||||||
static let optionHeight: CGFloat = 44
|
|
||||||
static let verticalMargin: CGFloat = 5
|
|
||||||
static let checkmarkImageSize = CGSize(width: 26, height: 26)
|
|
||||||
|
|
||||||
private var viewStateDisposeBag = Set<AnyCancellable>()
|
|
||||||
var attribute: PollItem.Attribute?
|
var attribute: PollItem.Attribute?
|
||||||
|
|
||||||
let roundedBackgroundView = UIView()
|
|
||||||
let voteProgressStripView: StripProgressView = {
|
|
||||||
let view = StripProgressView()
|
|
||||||
view.tintColor = Asset.Colors.Background.Poll.highlight.color
|
|
||||||
return view
|
|
||||||
}()
|
|
||||||
|
|
||||||
let checkmarkBackgroundView: UIView = {
|
|
||||||
let view = UIView()
|
|
||||||
view.backgroundColor = .systemBackground
|
|
||||||
return view
|
|
||||||
}()
|
|
||||||
|
|
||||||
let checkmarkImageView: UIView = {
|
|
||||||
let imageView = UIImageView()
|
|
||||||
let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 14, weight: .bold))!
|
|
||||||
imageView.image = image.withRenderingMode(.alwaysTemplate)
|
|
||||||
imageView.tintColor = Asset.Colors.Button.normal.color
|
|
||||||
return imageView
|
|
||||||
}()
|
|
||||||
|
|
||||||
let optionLabel: UILabel = {
|
|
||||||
let label = UILabel()
|
|
||||||
label.font = .systemFont(ofSize: 15, weight: .medium)
|
|
||||||
label.textColor = Asset.Colors.Label.primary.color
|
|
||||||
label.text = "Option"
|
|
||||||
label.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .left : .right
|
|
||||||
return label
|
|
||||||
}()
|
|
||||||
|
|
||||||
let optionLabelMiddlePaddingView = UIView()
|
|
||||||
|
|
||||||
let optionPercentageLabel: UILabel = {
|
|
||||||
let label = UILabel()
|
|
||||||
label.font = .systemFont(ofSize: 13, weight: .regular)
|
|
||||||
label.textColor = Asset.Colors.Label.primary.color
|
|
||||||
label.text = "50%"
|
|
||||||
label.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .right : .left
|
|
||||||
return label
|
|
||||||
}()
|
|
||||||
|
|
||||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
_init()
|
_init()
|
||||||
|
@ -76,7 +30,7 @@ final class PollOptionTableViewCell: UITableViewCell {
|
||||||
switch voteState {
|
switch voteState {
|
||||||
case .hidden:
|
case .hidden:
|
||||||
let color = Asset.Colors.Background.systemGroupedBackground.color
|
let color = Asset.Colors.Background.systemGroupedBackground.color
|
||||||
self.roundedBackgroundView.backgroundColor = isHighlighted ? color.withAlphaComponent(0.8) : color
|
pollOptionView.roundedBackgroundView.backgroundColor = isHighlighted ? color.withAlphaComponent(0.8) : color
|
||||||
case .reveal:
|
case .reveal:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -89,7 +43,7 @@ final class PollOptionTableViewCell: UITableViewCell {
|
||||||
switch voteState {
|
switch voteState {
|
||||||
case .hidden:
|
case .hidden:
|
||||||
let color = Asset.Colors.Background.systemGroupedBackground.color
|
let color = Asset.Colors.Background.systemGroupedBackground.color
|
||||||
self.roundedBackgroundView.backgroundColor = isHighlighted ? color.withAlphaComponent(0.8) : color
|
pollOptionView.roundedBackgroundView.backgroundColor = isHighlighted ? color.withAlphaComponent(0.8) : color
|
||||||
case .reveal:
|
case .reveal:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -102,125 +56,55 @@ extension PollOptionTableViewCell {
|
||||||
private func _init() {
|
private func _init() {
|
||||||
selectionStyle = .none
|
selectionStyle = .none
|
||||||
backgroundColor = .clear
|
backgroundColor = .clear
|
||||||
roundedBackgroundView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
pollOptionView.optionTextField.isUserInteractionEnabled = false
|
||||||
|
|
||||||
roundedBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
pollOptionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentView.addSubview(roundedBackgroundView)
|
contentView.addSubview(pollOptionView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
roundedBackgroundView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5),
|
pollOptionView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
roundedBackgroundView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
pollOptionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
roundedBackgroundView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
pollOptionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||||
contentView.bottomAnchor.constraint(equalTo: roundedBackgroundView.bottomAnchor, constant: 5),
|
pollOptionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||||
roundedBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionTableViewCell.optionHeight).priority(.defaultHigh),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
voteProgressStripView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
roundedBackgroundView.addSubview(voteProgressStripView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
voteProgressStripView.topAnchor.constraint(equalTo: roundedBackgroundView.topAnchor),
|
|
||||||
voteProgressStripView.leadingAnchor.constraint(equalTo: roundedBackgroundView.leadingAnchor),
|
|
||||||
voteProgressStripView.trailingAnchor.constraint(equalTo: roundedBackgroundView.trailingAnchor),
|
|
||||||
voteProgressStripView.bottomAnchor.constraint(equalTo: roundedBackgroundView.bottomAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
checkmarkBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
roundedBackgroundView.addSubview(checkmarkBackgroundView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
checkmarkBackgroundView.topAnchor.constraint(equalTo: roundedBackgroundView.topAnchor, constant: 9),
|
|
||||||
checkmarkBackgroundView.leadingAnchor.constraint(equalTo: roundedBackgroundView.leadingAnchor, constant: 9),
|
|
||||||
roundedBackgroundView.bottomAnchor.constraint(equalTo: checkmarkBackgroundView.bottomAnchor, constant: 9),
|
|
||||||
checkmarkBackgroundView.widthAnchor.constraint(equalToConstant: PollOptionTableViewCell.checkmarkImageSize.width).priority(.defaultHigh),
|
|
||||||
checkmarkBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionTableViewCell.checkmarkImageSize.height).priority(.defaultHigh),
|
|
||||||
])
|
|
||||||
|
|
||||||
checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
checkmarkBackgroundView.addSubview(checkmarkImageView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
checkmarkImageView.topAnchor.constraint(equalTo: checkmarkBackgroundView.topAnchor, constant: 5),
|
|
||||||
checkmarkImageView.leadingAnchor.constraint(equalTo: checkmarkBackgroundView.leadingAnchor, constant: 5),
|
|
||||||
checkmarkBackgroundView.trailingAnchor.constraint(equalTo: checkmarkImageView.trailingAnchor, constant: 5),
|
|
||||||
checkmarkBackgroundView.bottomAnchor.constraint(equalTo: checkmarkImageView.bottomAnchor, constant: 5),
|
|
||||||
])
|
|
||||||
|
|
||||||
optionLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
roundedBackgroundView.addSubview(optionLabel)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
optionLabel.leadingAnchor.constraint(equalTo: checkmarkBackgroundView.trailingAnchor, constant: 14),
|
|
||||||
optionLabel.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
optionLabelMiddlePaddingView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
roundedBackgroundView.addSubview(optionLabelMiddlePaddingView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
optionLabelMiddlePaddingView.leadingAnchor.constraint(equalTo: optionLabel.trailingAnchor),
|
|
||||||
optionLabelMiddlePaddingView.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
|
||||||
optionLabelMiddlePaddingView.heightAnchor.constraint(equalToConstant: 4).priority(.defaultHigh),
|
|
||||||
optionLabelMiddlePaddingView.widthAnchor.constraint(greaterThanOrEqualToConstant: 8).priority(.defaultLow),
|
|
||||||
])
|
|
||||||
optionLabelMiddlePaddingView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
||||||
|
|
||||||
optionPercentageLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
roundedBackgroundView.addSubview(optionPercentageLabel)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
optionPercentageLabel.leadingAnchor.constraint(equalTo: optionLabelMiddlePaddingView.trailingAnchor),
|
|
||||||
roundedBackgroundView.trailingAnchor.constraint(equalTo: optionPercentageLabel.trailingAnchor, constant: 18),
|
|
||||||
optionPercentageLabel.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
|
||||||
])
|
|
||||||
optionPercentageLabel.setContentHuggingPriority(.required - 1, for: .horizontal)
|
|
||||||
optionPercentageLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutSubviews() {
|
|
||||||
super.layoutSubviews()
|
|
||||||
|
|
||||||
updateCornerRadius()
|
|
||||||
updateTextAppearance()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateCornerRadius() {
|
|
||||||
roundedBackgroundView.layer.masksToBounds = true
|
|
||||||
roundedBackgroundView.layer.cornerRadius = PollOptionTableViewCell.optionHeight * 0.5
|
|
||||||
roundedBackgroundView.layer.cornerCurve = .circular
|
|
||||||
|
|
||||||
checkmarkBackgroundView.layer.masksToBounds = true
|
|
||||||
checkmarkBackgroundView.layer.cornerRadius = PollOptionTableViewCell.checkmarkImageSize.width * 0.5
|
|
||||||
checkmarkBackgroundView.layer.cornerCurve = .circular
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTextAppearance() {
|
func updateTextAppearance() {
|
||||||
guard let voteState = attribute?.voteState else {
|
guard let voteState = attribute?.voteState else {
|
||||||
optionLabel.textColor = Asset.Colors.Label.primary.color
|
pollOptionView.optionTextField.textColor = Asset.Colors.Label.primary.color
|
||||||
optionLabel.layer.removeShadow()
|
pollOptionView.optionTextField.layer.removeShadow()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch voteState {
|
switch voteState {
|
||||||
case .hidden:
|
case .hidden:
|
||||||
optionLabel.textColor = Asset.Colors.Label.primary.color
|
pollOptionView.optionTextField.textColor = Asset.Colors.Label.primary.color
|
||||||
optionLabel.layer.removeShadow()
|
pollOptionView.optionTextField.layer.removeShadow()
|
||||||
case .reveal(_, let percentage, _):
|
case .reveal(_, let percentage, _):
|
||||||
if CGFloat(percentage) * voteProgressStripView.frame.width > optionLabelMiddlePaddingView.frame.minX {
|
if CGFloat(percentage) * pollOptionView.voteProgressStripView.frame.width > pollOptionView.optionLabelMiddlePaddingView.frame.minX {
|
||||||
optionLabel.textColor = .white
|
pollOptionView.optionTextField.textColor = .white
|
||||||
optionLabel.layer.setupShadow(x: 0, y: 0, blur: 4, spread: 0)
|
pollOptionView.optionTextField.layer.setupShadow(x: 0, y: 0, blur: 4, spread: 0)
|
||||||
} else {
|
} else {
|
||||||
optionLabel.textColor = Asset.Colors.Label.primary.color
|
pollOptionView.optionTextField.textColor = Asset.Colors.Label.primary.color
|
||||||
optionLabel.layer.removeShadow()
|
pollOptionView.optionTextField.layer.removeShadow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if CGFloat(percentage) * voteProgressStripView.frame.width > optionLabelMiddlePaddingView.frame.maxX {
|
if CGFloat(percentage) * pollOptionView.voteProgressStripView.frame.width > pollOptionView.optionLabelMiddlePaddingView.frame.maxX {
|
||||||
optionPercentageLabel.textColor = .white
|
pollOptionView.optionPercentageLabel.textColor = .white
|
||||||
optionPercentageLabel.layer.setupShadow(x: 0, y: 0, blur: 4, spread: 0)
|
pollOptionView.optionPercentageLabel.layer.setupShadow(x: 0, y: 0, blur: 4, spread: 0)
|
||||||
} else {
|
} else {
|
||||||
optionPercentageLabel.textColor = Asset.Colors.Label.primary.color
|
pollOptionView.optionPercentageLabel.textColor = Asset.Colors.Label.primary.color
|
||||||
optionPercentageLabel.layer.removeShadow()
|
pollOptionView.optionPercentageLabel.layer.removeShadow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
updateTextAppearance()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#if canImport(SwiftUI) && DEBUG
|
#if canImport(SwiftUI) && DEBUG
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
//
|
||||||
|
// DeleteBackwardResponseTextField.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol DeleteBackwardResponseTextFieldDelegate: class {
|
||||||
|
func deleteBackwardResponseTextField(_ textField: DeleteBackwardResponseTextField, textBeforeDelete: String?)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class DeleteBackwardResponseTextField: UITextField {
|
||||||
|
|
||||||
|
weak var deleteBackwardDelegate: DeleteBackwardResponseTextFieldDelegate?
|
||||||
|
|
||||||
|
override func deleteBackward() {
|
||||||
|
let text = self.text
|
||||||
|
super.deleteBackward()
|
||||||
|
deleteBackwardDelegate?.deleteBackwardResponseTextField(self, textBeforeDelete: text)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -96,15 +96,34 @@ extension Mastodon.API.Statuses {
|
||||||
public struct PublishStatusQuery: Codable, PostQuery {
|
public struct PublishStatusQuery: Codable, PostQuery {
|
||||||
public let status: String?
|
public let status: String?
|
||||||
public let mediaIDs: [String]?
|
public let mediaIDs: [String]?
|
||||||
|
public let pollOptions: [String]?
|
||||||
|
public let pollExpiresIn: Int?
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
public init(status: String?, mediaIDs: [String]?, pollOptions: [String]?, pollExpiresIn: Int?) {
|
||||||
case status
|
|
||||||
case mediaIDs = "media_ids"
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(status: String?, mediaIDs: [String]?) {
|
|
||||||
self.status = status
|
self.status = status
|
||||||
self.mediaIDs = mediaIDs
|
self.mediaIDs = mediaIDs
|
||||||
|
self.pollOptions = pollOptions
|
||||||
|
self.pollExpiresIn = pollExpiresIn
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentType: String? {
|
||||||
|
return Self.multipartContentType()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: Data? {
|
||||||
|
var data = Data()
|
||||||
|
|
||||||
|
status.flatMap { data.append(Data.multipart(key: "status", value: $0)) }
|
||||||
|
for mediaID in mediaIDs ?? [] {
|
||||||
|
data.append(Data.multipart(key: "media_ids[]", value: mediaID))
|
||||||
|
}
|
||||||
|
for pollOption in pollOptions ?? [] {
|
||||||
|
data.append(Data.multipart(key: "poll[options][]", value: pollOption))
|
||||||
|
}
|
||||||
|
pollExpiresIn.flatMap { data.append(Data.multipart(key: "poll[expires_in]", value: $0)) }
|
||||||
|
|
||||||
|
data.append(Data.multipartEnd())
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,3 +35,12 @@ extension String: MultipartFormValue {
|
||||||
var multipartContentType: String? { return nil }
|
var multipartContentType: String? { return nil }
|
||||||
var multipartFilename: String? { return nil }
|
var multipartFilename: String? { return nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension Int: MultipartFormValue {
|
||||||
|
var multipartValue: Data {
|
||||||
|
return String(self).data(using: .utf8)!
|
||||||
|
}
|
||||||
|
var multipartContentType: String? { return nil }
|
||||||
|
var multipartFilename: String? { return nil }
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue